perspectapi-ts-sdk 1.5.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import { BaseClient } from './base-client';
6
+ import type { CacheManager } from '../cache/cache-manager';
6
7
  import type {
7
8
  ContactSubmission,
8
9
  CreateContactRequest,
@@ -13,8 +14,8 @@ import type {
13
14
  } from '../types';
14
15
 
15
16
  export class ContactClient extends BaseClient {
16
- constructor(http: any) {
17
- super(http, '/api/v1');
17
+ constructor(http: any, cache?: CacheManager) {
18
+ super(http, '/api/v1', cache);
18
19
  }
19
20
 
20
21
  /**
@@ -3,6 +3,8 @@
3
3
  */
4
4
 
5
5
  import { BaseClient } from './base-client';
6
+ import type { CacheManager } from '../cache/cache-manager';
7
+ import type { CachePolicy } from '../cache/types';
6
8
  import type {
7
9
  Content,
8
10
  CreateContentRequest,
@@ -13,31 +15,64 @@ import type {
13
15
  } from '../types';
14
16
 
15
17
  export class ContentClient extends BaseClient {
16
- constructor(http: any) {
17
- super(http, '/api/v1');
18
+ constructor(http: any, cache?: CacheManager) {
19
+ super(http, '/api/v1', cache);
18
20
  }
19
21
 
20
22
  /**
21
23
  * Get all content with pagination and filtering for a site
22
24
  */
23
- async getContent(siteName: string, params?: ContentQueryParams): Promise<PaginatedResponse<Content>> {
24
- return this.http.get(this.buildPath(this.siteScopedEndpoint(siteName)), params);
25
+ async getContent(
26
+ siteName: string,
27
+ params?: ContentQueryParams,
28
+ cachePolicy?: CachePolicy
29
+ ): Promise<PaginatedResponse<Content>> {
30
+ const endpoint = this.siteScopedEndpoint(siteName);
31
+ const path = this.buildPath(endpoint);
32
+
33
+ return this.fetchWithCache<PaginatedResponse<Content>>(
34
+ endpoint,
35
+ params,
36
+ this.buildContentTags(siteName),
37
+ cachePolicy,
38
+ () => this.http.get(path, params) as Promise<PaginatedResponse<Content>>
39
+ );
25
40
  }
26
41
 
27
42
  /**
28
43
  * Get content by ID
29
44
  */
30
- async getContentById(id: number): Promise<ApiResponse<Content>> {
31
- return this.getSingle<Content>(`/content/${id}`);
45
+ async getContentById(id: number, cachePolicy?: CachePolicy): Promise<ApiResponse<Content>> {
46
+ const endpoint = `/content/${id}`;
47
+ const path = this.buildPath(endpoint);
48
+
49
+ return this.fetchWithCache<ApiResponse<Content>>(
50
+ endpoint,
51
+ undefined,
52
+ this.buildContentTags(undefined, undefined, id),
53
+ cachePolicy,
54
+ () => this.http.get<Content>(path)
55
+ );
32
56
  }
33
57
 
34
58
  /**
35
59
  * Get content by slug for a site
36
60
  */
37
- async getContentBySlug(siteName: string, slug: string): Promise<ApiResponse<Content>> {
38
- return this.http.get(this.buildPath(
39
- this.siteScopedEndpoint(siteName, `/slug/${encodeURIComponent(slug)}`)
40
- ));
61
+ async getContentBySlug(
62
+ siteName: string,
63
+ slug: string,
64
+ cachePolicy?: CachePolicy
65
+ ): Promise<ApiResponse<Content>> {
66
+ const endpoint = this.siteScopedEndpoint(siteName, `/slug/${encodeURIComponent(slug)}`);
67
+ const path = this.buildPath(endpoint);
68
+
69
+ return this.fetchWithCache<ApiResponse<Content>>(
70
+ endpoint,
71
+ undefined,
72
+ this.buildContentTags(siteName, slug),
73
+ cachePolicy,
74
+ () => this.http.get<Content>(path)
75
+ );
41
76
  }
42
77
 
43
78
  /**
@@ -109,4 +144,18 @@ export class ContentClient extends BaseClient {
109
144
  async duplicateContent(id: number): Promise<ApiResponse<Content>> {
110
145
  return this.create<Record<string, never>, Content>(`/content/${id}/duplicate`, {});
111
146
  }
147
+
148
+ private buildContentTags(siteName?: string, slug?: string, id?: number): string[] {
149
+ const tags = new Set<string>(['content']);
150
+ if (siteName) {
151
+ tags.add(`content:site:${siteName}`);
152
+ }
153
+ if (slug) {
154
+ tags.add(`content:slug:${siteName}:${slug}`);
155
+ }
156
+ if (typeof id === 'number') {
157
+ tags.add(`content:id:${id}`);
158
+ }
159
+ return Array.from(tags.values());
160
+ }
112
161
  }
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import { BaseClient } from './base-client';
6
+ import type { CacheManager } from '../cache/cache-manager';
6
7
  import type {
7
8
  NewsletterSubscription,
8
9
  CreateNewsletterSubscriptionRequest,
@@ -18,8 +19,8 @@ import type {
18
19
  } from '../types';
19
20
 
20
21
  export class NewsletterClient extends BaseClient {
21
- constructor(http: any) {
22
- super(http, '/api/v1');
22
+ constructor(http: any, cache?: CacheManager) {
23
+ super(http, '/api/v1', cache);
23
24
  }
24
25
 
25
26
  /**
@@ -379,4 +380,4 @@ export class NewsletterClient extends BaseClient {
379
380
  data
380
381
  );
381
382
  }
382
- }
383
+ }
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import { BaseClient } from './base-client';
6
+ import type { CacheManager } from '../cache/cache-manager';
6
7
  import type {
7
8
  Organization,
8
9
  CreateOrganizationRequest,
@@ -11,8 +12,8 @@ import type {
11
12
  } from '../types';
12
13
 
13
14
  export class OrganizationsClient extends BaseClient {
14
- constructor(http: any) {
15
- super(http, '/api/v1');
15
+ constructor(http: any, cache?: CacheManager) {
16
+ super(http, '/api/v1', cache);
16
17
  }
17
18
 
18
19
  /**
@@ -3,6 +3,8 @@
3
3
  */
4
4
 
5
5
  import { BaseClient } from './base-client';
6
+ import type { CacheManager } from '../cache/cache-manager';
7
+ import type { CachePolicy } from '../cache/types';
6
8
  import type {
7
9
  Product,
8
10
  CreateProductRequest,
@@ -12,14 +14,18 @@ import type {
12
14
  } from '../types';
13
15
 
14
16
  export class ProductsClient extends BaseClient {
15
- constructor(http: any) {
16
- super(http, '/api/v1');
17
+ constructor(http: any, cache?: CacheManager) {
18
+ super(http, '/api/v1', cache);
17
19
  }
18
20
 
19
21
  /**
20
22
  * Get all products for a site
21
23
  */
22
- async getProducts(siteName: string, params?: ProductQueryParams): Promise<PaginatedResponse<Product>> {
24
+ async getProducts(
25
+ siteName: string,
26
+ params?: ProductQueryParams,
27
+ cachePolicy?: CachePolicy
28
+ ): Promise<PaginatedResponse<Product>> {
23
29
  const normalizeList = (value: string | number | Array<string | number> | undefined): string | undefined => {
24
30
  if (value === undefined || value === null) {
25
31
  return undefined;
@@ -50,33 +56,72 @@ export class ProductsClient extends BaseClient {
50
56
  }
51
57
  }
52
58
 
53
- return this.http.get(
54
- this.buildPath(this.siteScopedEndpoint(siteName, '/products', { includeSitesSegment: false })),
55
- normalizedParams
59
+ const endpoint = this.siteScopedEndpoint(siteName, '/products', { includeSitesSegment: false });
60
+ const path = this.buildPath(endpoint);
61
+
62
+ return this.fetchWithCache<PaginatedResponse<Product>>(
63
+ endpoint,
64
+ normalizedParams,
65
+ this.buildProductTags(siteName, ['products:list']),
66
+ cachePolicy,
67
+ () => this.http.get(path, normalizedParams) as Promise<PaginatedResponse<Product>>
56
68
  );
57
69
  }
58
70
 
59
71
  /**
60
72
  * Get product by ID
61
73
  */
62
- async getProductById(id: number): Promise<ApiResponse<Product>> {
63
- return this.getSingle<Product>(`/products/${id}`);
74
+ async getProductById(id: number, cachePolicy?: CachePolicy): Promise<ApiResponse<Product>> {
75
+ const endpoint = `/products/${id}`;
76
+ const path = this.buildPath(endpoint);
77
+
78
+ return this.fetchWithCache<ApiResponse<Product>>(
79
+ endpoint,
80
+ undefined,
81
+ this.buildProductTags(undefined, [`products:id:${id}`]),
82
+ cachePolicy,
83
+ () => this.http.get<Product>(path)
84
+ );
64
85
  }
65
86
 
66
87
  /**
67
88
  * Get product by SKU
68
89
  */
69
- async getProductBySku(sku: string): Promise<ApiResponse<Product>> {
70
- return this.getSingle<Product>(`/products/sku/${sku}`);
90
+ async getProductBySku(sku: string, cachePolicy?: CachePolicy): Promise<ApiResponse<Product>> {
91
+ const endpoint = `/products/sku/${encodeURIComponent(sku)}`;
92
+ const path = this.buildPath(endpoint);
93
+
94
+ return this.fetchWithCache<ApiResponse<Product>>(
95
+ endpoint,
96
+ undefined,
97
+ this.buildProductTags(undefined, [`products:sku:${sku.toLowerCase()}`]),
98
+ cachePolicy,
99
+ () => this.http.get<Product>(path)
100
+ );
71
101
  }
72
102
 
73
103
  /**
74
104
  * Get product by slug and site name
75
105
  */
76
- async getProductBySlug(siteName: string, slug: string): Promise<ApiResponse<Product & { variants?: any[] }>> {
77
- return this.http.get(this.buildPath(
78
- this.siteScopedEndpoint(siteName, `/products/slug/${encodeURIComponent(slug)}`, { includeSitesSegment: false })
79
- ));
106
+ async getProductBySlug(
107
+ siteName: string,
108
+ slug: string,
109
+ cachePolicy?: CachePolicy
110
+ ): Promise<ApiResponse<Product & { variants?: any[] }>> {
111
+ const endpoint = this.siteScopedEndpoint(
112
+ siteName,
113
+ `/products/slug/${encodeURIComponent(slug)}`,
114
+ { includeSitesSegment: false }
115
+ );
116
+ const path = this.buildPath(endpoint);
117
+
118
+ return this.fetchWithCache<ApiResponse<Product & { variants?: any[] }>>(
119
+ endpoint,
120
+ undefined,
121
+ this.buildProductTags(siteName, [`products:slug:${siteName}:${slug}`]),
122
+ cachePolicy,
123
+ () => this.http.get<Product & { variants?: any[] }>(path)
124
+ );
80
125
  }
81
126
 
82
127
  /**
@@ -221,7 +266,8 @@ export class ProductsClient extends BaseClient {
221
266
  limit?: number;
222
267
  published?: boolean;
223
268
  search?: string;
224
- }
269
+ },
270
+ cachePolicy?: CachePolicy
225
271
  ): Promise<ApiResponse<{
226
272
  data: Product[];
227
273
  category: {
@@ -243,12 +289,59 @@ export class ProductsClient extends BaseClient {
243
289
  search: params.search
244
290
  } : undefined;
245
291
 
246
- return this.http.get(this.buildPath(
247
- this.siteScopedEndpoint(
248
- siteName,
249
- `/products/category/${encodeURIComponent(categorySlug)}`,
250
- { includeSitesSegment: false }
251
- )
252
- ), queryParams);
292
+ const endpoint = this.siteScopedEndpoint(
293
+ siteName,
294
+ `/products/category/${encodeURIComponent(categorySlug)}`,
295
+ { includeSitesSegment: false }
296
+ );
297
+
298
+ const path = this.buildPath(endpoint);
299
+
300
+ return this.fetchWithCache<ApiResponse<{
301
+ data: Product[];
302
+ category: {
303
+ id: number;
304
+ name: string;
305
+ slug: string;
306
+ description?: string;
307
+ };
308
+ meta: {
309
+ count: number;
310
+ limit?: number;
311
+ offset?: number;
312
+ };
313
+ }>>(
314
+ endpoint,
315
+ queryParams,
316
+ this.buildProductTags(siteName, [
317
+ 'products:category',
318
+ `products:category:${siteName}:${categorySlug}`,
319
+ ]),
320
+ cachePolicy,
321
+ () =>
322
+ this.http.get<{
323
+ data: Product[];
324
+ category: {
325
+ id: number;
326
+ name: string;
327
+ slug: string;
328
+ description?: string;
329
+ };
330
+ meta: {
331
+ count: number;
332
+ limit?: number;
333
+ offset?: number;
334
+ };
335
+ }>(path, queryParams)
336
+ );
337
+ }
338
+
339
+ private buildProductTags(siteName?: string, extraTags: string[] = []): string[] {
340
+ const tags = new Set<string>(['products']);
341
+ if (siteName) {
342
+ tags.add(`products:site:${siteName}`);
343
+ }
344
+ extraTags.filter(Boolean).forEach(tag => tags.add(tag));
345
+ return Array.from(tags.values());
253
346
  }
254
347
  }
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import { BaseClient } from './base-client';
6
+ import type { CacheManager } from '../cache/cache-manager';
6
7
  import type {
7
8
  Site,
8
9
  CreateSiteRequest,
@@ -11,8 +12,8 @@ import type {
11
12
  } from '../types';
12
13
 
13
14
  export class SitesClient extends BaseClient {
14
- constructor(http: any) {
15
- super(http, '/api/v1');
15
+ constructor(http: any, cache?: CacheManager) {
16
+ super(http, '/api/v1', cache);
16
17
  }
17
18
 
18
19
  /**
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import { BaseClient } from './base-client';
6
+ import type { CacheManager } from '../cache/cache-manager';
6
7
  import type {
7
8
  Webhook,
8
9
  CreateWebhookRequest,
@@ -11,8 +12,8 @@ import type {
11
12
  } from '../types';
12
13
 
13
14
  export class WebhooksClient extends BaseClient {
14
- constructor(http: any) {
15
- super(http, '/api/v1');
15
+ constructor(http: any, cache?: CacheManager) {
16
+ super(http, '/api/v1', cache);
16
17
  }
17
18
 
18
19
  /**
package/src/index.ts CHANGED
@@ -26,6 +26,11 @@ export { BaseClient } from './client/base-client';
26
26
  // Utilities
27
27
  export { HttpClient, createApiError } from './utils/http-client';
28
28
 
29
+ // Cache utilities
30
+ export { CacheManager } from './cache/cache-manager';
31
+ export { InMemoryCacheAdapter } from './cache/in-memory-adapter';
32
+ export { NoopCacheAdapter } from './cache/noop-adapter';
33
+
29
34
  // Image transformation utilities
30
35
  export {
31
36
  buildImageUrl,
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { HttpClient } from './utils/http-client';
7
+ import { CacheManager } from './cache/cache-manager';
7
8
  import { AuthClient } from './client/auth-client';
8
9
  import { ContentClient } from './client/content-client';
9
10
  import { ApiKeysClient } from './client/api-keys-client';
@@ -20,6 +21,7 @@ import type { PerspectApiConfig, ApiResponse } from './types';
20
21
 
21
22
  export class PerspectApiClient {
22
23
  private http: HttpClient;
24
+ public readonly cache: CacheManager;
23
25
 
24
26
  // Service clients
25
27
  public readonly auth: AuthClient;
@@ -42,19 +44,20 @@ export class PerspectApiClient {
42
44
 
43
45
  // Initialize HTTP client
44
46
  this.http = new HttpClient(config);
47
+ this.cache = new CacheManager(config.cache);
45
48
 
46
49
  // Initialize service clients
47
- this.auth = new AuthClient(this.http);
48
- this.content = new ContentClient(this.http);
49
- this.apiKeys = new ApiKeysClient(this.http);
50
- this.organizations = new OrganizationsClient(this.http);
51
- this.sites = new SitesClient(this.http);
52
- this.products = new ProductsClient(this.http);
53
- this.categories = new CategoriesClient(this.http);
54
- this.webhooks = new WebhooksClient(this.http);
55
- this.checkout = new CheckoutClient(this.http);
56
- this.contact = new ContactClient(this.http);
57
- this.newsletter = new NewsletterClient(this.http);
50
+ this.auth = new AuthClient(this.http, this.cache);
51
+ this.content = new ContentClient(this.http, this.cache);
52
+ this.apiKeys = new ApiKeysClient(this.http, this.cache);
53
+ this.organizations = new OrganizationsClient(this.http, this.cache);
54
+ this.sites = new SitesClient(this.http, this.cache);
55
+ this.products = new ProductsClient(this.http, this.cache);
56
+ this.categories = new CategoriesClient(this.http, this.cache);
57
+ this.webhooks = new WebhooksClient(this.http, this.cache);
58
+ this.checkout = new CheckoutClient(this.http, this.cache);
59
+ this.contact = new ContactClient(this.http, this.cache);
60
+ this.newsletter = new NewsletterClient(this.http, this.cache);
58
61
  }
59
62
 
60
63
  /**
@@ -2,6 +2,8 @@
2
2
  * Core types and interfaces for PerspectAPI SDK
3
3
  */
4
4
 
5
+ import type { CacheConfig } from '../cache/types';
6
+
5
7
  // Base API Response
6
8
  export interface ApiResponse<T = any> {
7
9
  data?: T;
@@ -495,6 +497,7 @@ export interface PerspectApiConfig {
495
497
  timeout?: number;
496
498
  retries?: number;
497
499
  headers?: Record<string, string>;
500
+ cache?: CacheConfig;
498
501
  }
499
502
 
500
503
  // HTTP Methods
@@ -509,3 +512,6 @@ export interface RequestOptions {
509
512
  timeout?: number;
510
513
  csrfToken?: string; // Optional CSRF token for protected endpoints
511
514
  }
515
+
516
+ // Cache types re-exported for convenience
517
+ export type { CacheConfig } from '../cache/types';