Skip to content

Commit 1627f1a

Browse files
committed
feat: Introduce ContentstackPlugin interface for extensible plugin architecture
- Added ContentstackPlugin interface to support custom plugins in the Contentstack SDK. - Updated stack function to utilize plugins for request and response interception. - Implemented comprehensive unit tests for plugin functionality, including various implementation patterns and edge cases. - Enhanced type safety for plugins in StackConfig. - Created tests for plugin interactions with interceptors, error handling, and performance scenarios. - Added support for configurable plugins and state management within plugins.
1 parent ca2fc4b commit 1627f1a

6 files changed

Lines changed: 1702 additions & 4 deletions

File tree

.talismanrc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,12 @@ fileignoreconfig:
33
checksum: dffbcb14c5761976a9b6ab90b96c7e4fc5784eebe381cf48014618cc93355dbc
44
- filename: test/unit/query-optimization-comprehensive.spec.ts
55
checksum: f5aaf6c784d7c101a05ca513c584bbd6e95f963d1e42779f2596050d9bcbac96
6+
- filename: src/lib/types.ts
7+
checksum: 57d2f7eede6ab19ff92db9799e752e7dfa44c1368314525a41ab1a241778a230
8+
- filename: examples/custom-plugin.ts
9+
checksum: 51a4b01f7c5f76c97c588bb0575cefd1f9462b28d88b24bb9f7dc2780aa323c5
10+
- filename: test/unit/plugin-comprehensive.spec.ts
11+
checksum: d508f95f4a7eb5bed873ca91a5e1914aa4d5261f27593edb568502cb5855042e
12+
- filename: test/unit/plugin-interceptor-comprehensive.spec.ts
13+
checksum: b9fbe9da166fd209853307f9f60464d25f805f82e9dbe09d5f476fdd5eb05d47
614
version: ""

examples/custom-plugin.ts

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
/**
2+
* Example implementations of ContentstackPlugin interface
3+
* This file demonstrates various ways to create and use plugins
4+
*/
5+
import contentstack, { ContentstackPlugin } from '@contentstack/delivery-sdk';
6+
import { AxiosRequestConfig, AxiosResponse } from 'axios';
7+
8+
// Example 1: Simple logging plugin as a class
9+
class LoggingPlugin implements ContentstackPlugin {
10+
private requestCount = 0;
11+
12+
onRequest(config: AxiosRequestConfig): AxiosRequestConfig {
13+
this.requestCount++;
14+
console.log(`[Plugin] Request #${this.requestCount}: ${config.method?.toUpperCase()} ${config.url}`);
15+
16+
return {
17+
...config,
18+
headers: {
19+
...config.headers,
20+
'X-Request-ID': `req-${this.requestCount}-${Date.now()}`
21+
}
22+
};
23+
}
24+
25+
onResponse(request: AxiosRequestConfig, response: AxiosResponse, data: any): AxiosResponse {
26+
console.log(`[Plugin] Response: ${response.status} for ${request.url}`);
27+
28+
return {
29+
...response,
30+
data: {
31+
...data,
32+
_meta: {
33+
requestId: request.headers?.['X-Request-ID'],
34+
processedAt: new Date().toISOString()
35+
}
36+
}
37+
};
38+
}
39+
40+
getRequestCount(): number {
41+
return this.requestCount;
42+
}
43+
}
44+
45+
// Example 2: Authentication plugin as an object
46+
const authPlugin: ContentstackPlugin = {
47+
onRequest(config: AxiosRequestConfig): AxiosRequestConfig {
48+
return {
49+
...config,
50+
headers: {
51+
...config.headers,
52+
'X-Custom-Auth': 'Bearer my-custom-token',
53+
'X-Client-Version': '1.0.0'
54+
}
55+
};
56+
},
57+
58+
onResponse(request: AxiosRequestConfig, response: AxiosResponse, data: any): AxiosResponse {
59+
// Remove any sensitive information from the response
60+
if (data && typeof data === 'object') {
61+
const sanitized = { ...data };
62+
delete sanitized.internal_data;
63+
delete sanitized.debug_info;
64+
65+
return {
66+
...response,
67+
data: sanitized
68+
};
69+
}
70+
71+
return response;
72+
}
73+
};
74+
75+
// Example 3: Performance monitoring plugin
76+
class PerformancePlugin implements ContentstackPlugin {
77+
private requestTimes = new Map<string, number>();
78+
private metrics = {
79+
totalRequests: 0,
80+
totalTime: 0,
81+
slowRequests: 0,
82+
errors: 0
83+
};
84+
85+
onRequest(config: AxiosRequestConfig): AxiosRequestConfig {
86+
const requestId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
87+
this.requestTimes.set(requestId, performance.now());
88+
this.metrics.totalRequests++;
89+
90+
return {
91+
...config,
92+
metadata: {
93+
...config.metadata,
94+
requestId,
95+
startTime: performance.now()
96+
}
97+
};
98+
}
99+
100+
onResponse(request: AxiosRequestConfig, response: AxiosResponse, data: any): AxiosResponse {
101+
const requestId = request.metadata?.requestId;
102+
const startTime = this.requestTimes.get(requestId);
103+
104+
if (startTime) {
105+
const duration = performance.now() - startTime;
106+
this.metrics.totalTime += duration;
107+
108+
if (duration > 1000) { // Slow request threshold: 1 second
109+
this.metrics.slowRequests++;
110+
console.warn(`[Performance] Slow request detected: ${duration.toFixed(2)}ms for ${request.url}`);
111+
}
112+
113+
this.requestTimes.delete(requestId);
114+
}
115+
116+
if (response.status >= 400) {
117+
this.metrics.errors++;
118+
}
119+
120+
return {
121+
...response,
122+
data: {
123+
...data,
124+
_performance: {
125+
duration: startTime ? performance.now() - startTime : 0,
126+
timestamp: new Date().toISOString()
127+
}
128+
}
129+
};
130+
}
131+
132+
getMetrics() {
133+
return {
134+
...this.metrics,
135+
averageTime: this.metrics.totalRequests > 0
136+
? this.metrics.totalTime / this.metrics.totalRequests
137+
: 0
138+
};
139+
}
140+
141+
printReport() {
142+
const metrics = this.getMetrics();
143+
console.log(`
144+
Performance Report:
145+
- Total Requests: ${metrics.totalRequests}
146+
- Average Response Time: ${metrics.averageTime.toFixed(2)}ms
147+
- Slow Requests: ${metrics.slowRequests}
148+
- Errors: ${metrics.errors}
149+
`);
150+
}
151+
}
152+
153+
// Example 4: Caching plugin with TTL
154+
class CachePlugin implements ContentstackPlugin {
155+
private cache = new Map<string, { data: any; timestamp: number; ttl: number }>();
156+
private defaultTTL = 5 * 60 * 1000; // 5 minutes
157+
158+
onRequest(config: AxiosRequestConfig): AxiosRequestConfig {
159+
const cacheKey = this.generateCacheKey(config);
160+
const cached = this.cache.get(cacheKey);
161+
162+
if (cached && Date.now() - cached.timestamp < cached.ttl) {
163+
// In a real implementation, you might want to return cached data directly
164+
// This is just for demonstration
165+
console.log(`[Cache] Cache hit for ${cacheKey}`);
166+
return { ...config, fromCache: true };
167+
}
168+
169+
return { ...config, cacheKey };
170+
}
171+
172+
onResponse(request: AxiosRequestConfig, response: AxiosResponse, data: any): AxiosResponse {
173+
if (response.status === 200 && request.cacheKey && !request.fromCache) {
174+
this.cache.set(request.cacheKey, {
175+
data,
176+
timestamp: Date.now(),
177+
ttl: this.defaultTTL
178+
});
179+
console.log(`[Cache] Cached response for ${request.cacheKey}`);
180+
}
181+
182+
return response;
183+
}
184+
185+
private generateCacheKey(config: AxiosRequestConfig): string {
186+
return `${config.method || 'GET'}_${config.url}_${JSON.stringify(config.params || {})}`;
187+
}
188+
189+
clearCache(): void {
190+
this.cache.clear();
191+
}
192+
193+
getCacheStats() {
194+
return {
195+
size: this.cache.size,
196+
keys: Array.from(this.cache.keys())
197+
};
198+
}
199+
}
200+
201+
// Example 5: Error handling and retry plugin
202+
const errorHandlingPlugin: ContentstackPlugin = {
203+
onRequest(config: AxiosRequestConfig): AxiosRequestConfig {
204+
return {
205+
...config,
206+
retryAttempts: config.retryAttempts || 0,
207+
maxRetries: 3
208+
};
209+
},
210+
211+
onResponse(request: AxiosRequestConfig, response: AxiosResponse, data: any): AxiosResponse {
212+
if (response.status >= 400) {
213+
console.error(`[Error] HTTP ${response.status}: ${response.statusText} for ${request.url}`);
214+
215+
// Log additional error context
216+
if (response.status >= 500) {
217+
console.error('[Error] Server error detected. Consider implementing retry logic.');
218+
} else if (response.status === 429) {
219+
console.warn('[Error] Rate limit exceeded. Implementing backoff strategy recommended.');
220+
}
221+
}
222+
223+
return {
224+
...response,
225+
data: {
226+
...data,
227+
_error_handled: response.status >= 400,
228+
_error_code: response.status >= 400 ? response.status : null
229+
}
230+
};
231+
}
232+
};
233+
234+
// Usage example
235+
function createStackWithPlugins() {
236+
const loggingPlugin = new LoggingPlugin();
237+
const performancePlugin = new PerformancePlugin();
238+
const cachePlugin = new CachePlugin();
239+
240+
const stack = contentstack.stack({
241+
apiKey: 'your-api-key',
242+
deliveryToken: 'your-delivery-token',
243+
environment: 'your-environment',
244+
plugins: [
245+
loggingPlugin, // Logs all requests/responses
246+
authPlugin, // Adds authentication headers
247+
performancePlugin, // Monitors performance
248+
cachePlugin, // Caches responses
249+
errorHandlingPlugin // Handles errors gracefully
250+
]
251+
});
252+
253+
// You can access plugin methods if needed
254+
console.log(`Total requests made: ${loggingPlugin.getRequestCount()}`);
255+
256+
// Print performance report
257+
performancePlugin.printReport();
258+
259+
// Check cache stats
260+
console.log('Cache stats:', cachePlugin.getCacheStats());
261+
262+
return stack;
263+
}
264+
265+
// Export for use in other files
266+
export {
267+
LoggingPlugin,
268+
PerformancePlugin,
269+
CachePlugin,
270+
authPlugin,
271+
errorHandlingPlugin,
272+
createStackWithPlugins
273+
};

src/lib/contentstack.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { httpClient, retryRequestHandler, retryResponseErrorHandler, retryRespon
22
import { AxiosRequestHeaders } from 'axios';
33
import { handleRequest } from './cache';
44
import { Stack as StackClass } from './stack';
5-
import { Policy, StackConfig } from './types';
5+
import { Policy, StackConfig, ContentstackPlugin } from './types';
66
import * as Utility from './utils';
77
export * as Utils from '@contentstack/utils';
88

@@ -114,7 +114,7 @@ export function stack(config: StackConfig): StackClass {
114114
if (config.plugins) {
115115
client.interceptors.request.use((reqConfig: any): any => {
116116
if (config && config.plugins)
117-
config.plugins.forEach((pluginInstance) => {
117+
config.plugins.forEach((pluginInstance: ContentstackPlugin) => {
118118
reqConfig = pluginInstance.onRequest(reqConfig);
119119
});
120120

@@ -123,7 +123,7 @@ export function stack(config: StackConfig): StackClass {
123123

124124
client.interceptors.response.use((response: any) => {
125125
if (config && config.plugins)
126-
config.plugins.forEach((pluginInstance) => {
126+
config.plugins.forEach((pluginInstance: ContentstackPlugin) => {
127127
response = pluginInstance.onResponse(response.request, response, response.data);
128128
});
129129

src/lib/types.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* eslint-disable @cspell/spellchecker */
22
import { HttpClientParams } from "@contentstack/core";
33
import { PersistanceStoreOptions, StorageType } from "../persistance";
4+
import { AxiosRequestConfig, AxiosResponse } from "axios";
45

56
// Internal Types
67
export type params = {
@@ -11,6 +12,53 @@ export type queryParams = {
1112
[key: string]: string | boolean | number | string[];
1213
};
1314

15+
/**
16+
* Interface for creating Contentstack plugins
17+
*
18+
* @example
19+
* ```typescript
20+
* import { ContentstackPlugin } from '@contentstack/delivery-sdk';
21+
*
22+
* class MyPlugin implements ContentstackPlugin {
23+
* onRequest(config: any): any {
24+
* // Modify request configuration
25+
* console.log('Processing request:', config.url);
26+
* return { ...config, headers: { ...config.headers, 'X-Custom-Header': 'value' } };
27+
* }
28+
*
29+
* onResponse(request: any, response: any, data: any): any {
30+
* // Process response data
31+
* console.log('Processing response:', response.status);
32+
* return { ...response, data: { ...data, processed: true } };
33+
* }
34+
* }
35+
*
36+
* const stack = contentstack.stack({
37+
* apiKey: 'your-api-key',
38+
* deliveryToken: 'your-delivery-token',
39+
* environment: 'your-environment',
40+
* plugins: [new MyPlugin()]
41+
* });
42+
* ```
43+
*/
44+
export interface ContentstackPlugin {
45+
/**
46+
* Called before each request is sent
47+
* @param config - Axios request configuration object (with possible custom properties)
48+
* @returns Modified request configuration (can be sync or async)
49+
*/
50+
onRequest(config: any): any;
51+
52+
/**
53+
* Called after each response is received
54+
* @param request - The original request configuration
55+
* @param response - Axios response object (with possible custom properties)
56+
* @param data - Response data
57+
* @returns Modified response object (can be sync or async)
58+
*/
59+
onResponse(request: any, response: any, data: any): any;
60+
}
61+
1462
// External Types
1563
export enum Region {
1664
US = "us",
@@ -30,7 +78,7 @@ export interface StackConfig extends HttpClientParams {
3078
early_access?: string[];
3179
region?: Region;
3280
locale?: string;
33-
plugins?: any[];
81+
plugins?: ContentstackPlugin[];
3482
logHandler?: (level: string, data: any) => void;
3583
cacheOptions?: CacheOptions;
3684
live_preview?: LivePreview;

0 commit comments

Comments
 (0)