perspectapi-ts-sdk 5.0.2 → 5.1.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.
@@ -1,8 +1,10 @@
1
1
  /**
2
- * v2 Base Client — cursor pagination, expand support, typed errors.
2
+ * v2 Base Client — cursor pagination, expand support, caching, typed errors.
3
3
  */
4
4
 
5
5
  import { HttpClient } from '../../utils/http-client';
6
+ import type { CacheManager } from '../../cache/cache-manager';
7
+ import type { CachePolicy, CacheInvalidateOptions } from '../../cache/types';
6
8
  import type { V2List, V2Error, V2Deleted } from '../types';
7
9
 
8
10
  export class PerspectV2Error extends Error {
@@ -24,10 +26,12 @@ export class PerspectV2Error extends Error {
24
26
  export abstract class BaseV2Client {
25
27
  protected http: HttpClient;
26
28
  protected basePath: string;
29
+ protected cache?: CacheManager;
27
30
 
28
- constructor(http: HttpClient, basePath: string) {
31
+ constructor(http: HttpClient, basePath: string, cache?: CacheManager) {
29
32
  this.http = http;
30
33
  this.basePath = basePath;
34
+ this.cache = cache && cache.isEnabled() ? cache : undefined;
31
35
  }
32
36
 
33
37
  protected buildPath(endpoint: string): string {
@@ -77,19 +81,26 @@ export abstract class BaseV2Client {
77
81
  return payload as T;
78
82
  }
79
83
 
80
- /** GET a single resource. */
81
- protected async getOne<T>(path: string, params?: object): Promise<T> {
82
- const response = await this.http.get<T>(path, this.toParams(params));
83
- return this.extractData<T>(response);
84
+ /** GET a single resource, with optional caching. */
85
+ protected async getOne<T>(path: string, params?: object, cachePolicy?: CachePolicy): Promise<T> {
86
+ const fetcher = async () => {
87
+ const response = await this.http.get<T>(path, this.toParams(params));
88
+ return this.extractData<T>(response);
89
+ };
90
+ return this.fetchWithCache(path, params, cachePolicy, fetcher);
84
91
  }
85
92
 
86
- /** GET a list of resources with cursor pagination. */
93
+ /** GET a list of resources with cursor pagination, with optional caching. */
87
94
  protected async getList<T>(
88
95
  path: string,
89
96
  params?: object,
97
+ cachePolicy?: CachePolicy,
90
98
  ): Promise<V2List<T>> {
91
- const response = await this.http.get<V2List<T>>(path, this.toParams(params));
92
- return this.extractData<V2List<T>>(response);
99
+ const fetcher = async () => {
100
+ const response = await this.http.get<V2List<T>>(path, this.toParams(params));
101
+ return this.extractData<V2List<T>>(response);
102
+ };
103
+ return this.fetchWithCache(path, params, cachePolicy, fetcher);
93
104
  }
94
105
 
95
106
  /** POST to create a resource. */
@@ -110,6 +121,41 @@ export abstract class BaseV2Client {
110
121
  return this.extractData<V2Deleted>(response);
111
122
  }
112
123
 
124
+ /** Fetch with optional cache. Bypasses cache for writes or when no cache is configured. */
125
+ private async fetchWithCache<T>(
126
+ path: string,
127
+ params: object | undefined,
128
+ policy: CachePolicy | undefined,
129
+ fetcher: () => Promise<T>,
130
+ ): Promise<T> {
131
+ if (!this.cache || policy?.skipCache) {
132
+ return fetcher();
133
+ }
134
+ const key = this.buildCacheKey(path, params);
135
+ return this.cache.getOrSet<T>(key, fetcher, policy);
136
+ }
137
+
138
+ /** Invalidate cache entries by keys or tags. */
139
+ protected async invalidateCache(options: CacheInvalidateOptions): Promise<void> {
140
+ if (!this.cache) return;
141
+ await this.cache.invalidate(options);
142
+ }
143
+
144
+ private buildCacheKey(path: string, params?: object): string {
145
+ const parts: Array<string | Record<string, unknown>> = [path];
146
+ if (params && Object.keys(params).length > 0) {
147
+ const sorted: Record<string, unknown> = {};
148
+ for (const key of Object.keys(params).sort()) {
149
+ sorted[key] = (params as Record<string, unknown>)[key];
150
+ }
151
+ parts.push(sorted);
152
+ }
153
+ if (this.cache) {
154
+ return this.cache.buildKey(parts);
155
+ }
156
+ return parts.map(p => typeof p === 'string' ? p : JSON.stringify(p)).join(':');
157
+ }
158
+
113
159
  /**
114
160
  * Auto-paginating async generator.
115
161
  * Yields every item across all pages.
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import { BaseV2Client } from './base-v2-client';
6
+ import type { CachePolicy } from '../../cache/types';
6
7
  import type {
7
8
  V2Category, V2CategoryCreateParams, V2CategoryUpdateParams,
8
9
  V2PaginationParams, V2List, V2Deleted,
@@ -10,12 +11,12 @@ import type {
10
11
 
11
12
  export class CategoriesV2Client extends BaseV2Client {
12
13
 
13
- async list(siteName: string, params?: V2PaginationParams & { type?: string }): Promise<V2List<V2Category>> {
14
- return this.getList<V2Category>(this.sitePath(siteName, 'categories'), params);
14
+ async list(siteName: string, params?: V2PaginationParams & { type?: string }, cachePolicy?: CachePolicy): Promise<V2List<V2Category>> {
15
+ return this.getList<V2Category>(this.sitePath(siteName, 'categories'), params, cachePolicy);
15
16
  }
16
17
 
17
- async get(siteName: string, id: string): Promise<V2Category> {
18
- return this.getOne<V2Category>(this.sitePath(siteName, 'categories', id));
18
+ async get(siteName: string, id: string, cachePolicy?: CachePolicy): Promise<V2Category> {
19
+ return this.getOne<V2Category>(this.sitePath(siteName, 'categories', id), undefined, cachePolicy);
19
20
  }
20
21
 
21
22
  async create(siteName: string, data: V2CategoryCreateParams): Promise<V2Category> {
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import { BaseV2Client } from './base-v2-client';
6
+ import type { CachePolicy } from '../../cache/types';
6
7
  import type {
7
8
  V2Content, V2ContentCreateParams, V2ContentUpdateParams,
8
9
  V2ContentListParams, V2List, V2Deleted,
@@ -10,16 +11,16 @@ import type {
10
11
 
11
12
  export class ContentV2Client extends BaseV2Client {
12
13
 
13
- async list(siteName: string, params?: V2ContentListParams): Promise<V2List<V2Content>> {
14
- return this.getList<V2Content>(this.sitePath(siteName, 'content'), params);
14
+ async list(siteName: string, params?: V2ContentListParams, cachePolicy?: CachePolicy): Promise<V2List<V2Content>> {
15
+ return this.getList<V2Content>(this.sitePath(siteName, 'content'), params, cachePolicy);
15
16
  }
16
17
 
17
18
  async *listAutoPaginated(siteName: string, params?: Omit<V2ContentListParams, 'starting_after' | 'ending_before'>) {
18
19
  yield* this.listAutoPaginate<V2Content>(this.sitePath(siteName, 'content'), params);
19
20
  }
20
21
 
21
- async get(siteName: string, idOrSlug: string): Promise<V2Content> {
22
- return this.getOne<V2Content>(this.sitePath(siteName, 'content', idOrSlug));
22
+ async get(siteName: string, idOrSlug: string, cachePolicy?: CachePolicy): Promise<V2Content> {
23
+ return this.getOne<V2Content>(this.sitePath(siteName, 'content', idOrSlug), undefined, cachePolicy);
23
24
  }
24
25
 
25
26
  async create(siteName: string, data: V2ContentCreateParams): Promise<V2Content> {
@@ -3,9 +3,10 @@
3
3
  */
4
4
 
5
5
  import { BaseV2Client } from './base-v2-client';
6
+ import type { CachePolicy } from '../../cache/types';
6
7
  import type {
7
8
  V2NewsletterSubscription, V2NewsletterList, V2NewsletterCampaign,
8
- V2PaginationParams, V2List,
9
+ V2PaginationParams, V2List, V2NewsletterTrackingResponse,
9
10
  } from '../types';
10
11
 
11
12
  export class NewsletterV2Client extends BaseV2Client {
@@ -48,29 +49,55 @@ export class NewsletterV2Client extends BaseV2Client {
48
49
  );
49
50
  }
50
51
 
52
+ async trackOpen(
53
+ siteName: string,
54
+ token: string,
55
+ ): Promise<V2NewsletterTrackingResponse> {
56
+ return this.post<V2NewsletterTrackingResponse>(
57
+ this.sitePath(siteName, 'newsletter', `track/open/${encodeURIComponent(token)}`),
58
+ );
59
+ }
60
+
61
+ async trackClick(
62
+ siteName: string,
63
+ token: string,
64
+ url: string,
65
+ ): Promise<V2NewsletterTrackingResponse> {
66
+ return this.post<V2NewsletterTrackingResponse>(
67
+ this.sitePath(siteName, 'newsletter', `track/click/${encodeURIComponent(token)}`),
68
+ { url },
69
+ );
70
+ }
71
+
51
72
  // --- Subscriptions (admin) ---
52
73
 
53
74
  async listSubscriptions(
54
75
  siteName: string,
55
76
  params?: V2PaginationParams & { status?: string },
77
+ cachePolicy?: CachePolicy,
56
78
  ): Promise<V2List<V2NewsletterSubscription>> {
57
79
  return this.getList<V2NewsletterSubscription>(
58
80
  this.sitePath(siteName, 'newsletter', 'subscriptions'),
59
81
  params,
82
+ cachePolicy,
60
83
  );
61
84
  }
62
85
 
63
- async getSubscription(siteName: string, id: string): Promise<V2NewsletterSubscription> {
86
+ async getSubscription(siteName: string, id: string, cachePolicy?: CachePolicy): Promise<V2NewsletterSubscription> {
64
87
  return this.getOne<V2NewsletterSubscription>(
65
88
  this.sitePath(siteName, 'newsletter', `subscriptions/${id}`),
89
+ undefined,
90
+ cachePolicy,
66
91
  );
67
92
  }
68
93
 
69
94
  // --- Lists ---
70
95
 
71
- async listLists(siteName: string): Promise<V2List<V2NewsletterList>> {
96
+ async listLists(siteName: string, cachePolicy?: CachePolicy): Promise<V2List<V2NewsletterList>> {
72
97
  return this.getList<V2NewsletterList>(
73
98
  this.sitePath(siteName, 'newsletter', 'lists'),
99
+ undefined,
100
+ cachePolicy,
74
101
  );
75
102
  }
76
103
 
@@ -79,16 +106,20 @@ export class NewsletterV2Client extends BaseV2Client {
79
106
  async listCampaigns(
80
107
  siteName: string,
81
108
  params?: V2PaginationParams & { status?: string },
109
+ cachePolicy?: CachePolicy,
82
110
  ): Promise<V2List<V2NewsletterCampaign>> {
83
111
  return this.getList<V2NewsletterCampaign>(
84
112
  this.sitePath(siteName, 'newsletter', 'campaigns'),
85
113
  params,
114
+ cachePolicy,
86
115
  );
87
116
  }
88
117
 
89
- async getCampaign(siteName: string, idOrSlug: string): Promise<V2NewsletterCampaign> {
118
+ async getCampaign(siteName: string, idOrSlug: string, cachePolicy?: CachePolicy): Promise<V2NewsletterCampaign> {
90
119
  return this.getOne<V2NewsletterCampaign>(
91
120
  this.sitePath(siteName, 'newsletter', `campaigns/${idOrSlug}`),
121
+ undefined,
122
+ cachePolicy,
92
123
  );
93
124
  }
94
125
  }
@@ -3,19 +3,20 @@
3
3
  */
4
4
 
5
5
  import { BaseV2Client } from './base-v2-client';
6
+ import type { CachePolicy } from '../../cache/types';
6
7
  import type { V2Order, V2OrderListParams, V2List } from '../types';
7
8
 
8
9
  export class OrdersV2Client extends BaseV2Client {
9
10
 
10
- async list(siteName: string, params?: V2OrderListParams): Promise<V2List<V2Order>> {
11
- return this.getList<V2Order>(this.sitePath(siteName, 'orders'), params);
11
+ async list(siteName: string, params?: V2OrderListParams, cachePolicy?: CachePolicy): Promise<V2List<V2Order>> {
12
+ return this.getList<V2Order>(this.sitePath(siteName, 'orders'), params, cachePolicy);
12
13
  }
13
14
 
14
15
  async *listAutoPaginated(siteName: string, params?: Omit<V2OrderListParams, 'starting_after' | 'ending_before'>) {
15
16
  yield* this.listAutoPaginate<V2Order>(this.sitePath(siteName, 'orders'), params);
16
17
  }
17
18
 
18
- async get(siteName: string, id: string): Promise<V2Order> {
19
- return this.getOne<V2Order>(this.sitePath(siteName, 'orders', id));
19
+ async get(siteName: string, id: string, cachePolicy?: CachePolicy): Promise<V2Order> {
20
+ return this.getOne<V2Order>(this.sitePath(siteName, 'orders', id), undefined, cachePolicy);
20
21
  }
21
22
  }
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import { BaseV2Client } from './base-v2-client';
6
+ import type { CachePolicy } from '../../cache/types';
6
7
  import type {
7
8
  V2Product, V2ProductCreateParams, V2ProductUpdateParams,
8
9
  V2ProductListParams, V2List, V2Deleted,
@@ -10,16 +11,16 @@ import type {
10
11
 
11
12
  export class ProductsV2Client extends BaseV2Client {
12
13
 
13
- async list(siteName: string, params?: V2ProductListParams): Promise<V2List<V2Product>> {
14
- return this.getList<V2Product>(this.sitePath(siteName, 'products'), params);
14
+ async list(siteName: string, params?: V2ProductListParams, cachePolicy?: CachePolicy): Promise<V2List<V2Product>> {
15
+ return this.getList<V2Product>(this.sitePath(siteName, 'products'), params, cachePolicy);
15
16
  }
16
17
 
17
18
  async *listAutoPaginated(siteName: string, params?: Omit<V2ProductListParams, 'starting_after' | 'ending_before'>) {
18
19
  yield* this.listAutoPaginate<V2Product>(this.sitePath(siteName, 'products'), params);
19
20
  }
20
21
 
21
- async get(siteName: string, idOrSlug: string): Promise<V2Product> {
22
- return this.getOne<V2Product>(this.sitePath(siteName, 'products', idOrSlug));
22
+ async get(siteName: string, idOrSlug: string, cachePolicy?: CachePolicy): Promise<V2Product> {
23
+ return this.getOne<V2Product>(this.sitePath(siteName, 'products', idOrSlug), undefined, cachePolicy);
23
24
  }
24
25
 
25
26
  async create(siteName: string, data: V2ProductCreateParams): Promise<V2Product> {
package/src/v2/index.ts CHANGED
@@ -11,7 +11,9 @@
11
11
  */
12
12
 
13
13
  import { HttpClient } from '../utils/http-client';
14
+ import { CacheManager } from '../cache/cache-manager';
14
15
  import type { PerspectApiConfig } from '../types';
16
+ import type { CacheConfig } from '../cache/types';
15
17
 
16
18
  import { ContentV2Client } from './client/content-client';
17
19
  import { ProductsV2Client } from './client/products-client';
@@ -26,8 +28,13 @@ import { SitesV2Client } from './client/sites-client';
26
28
  import { ApiKeysV2Client } from './client/api-keys-client';
27
29
  import { WebhooksV2Client } from './client/webhooks-client';
28
30
 
31
+ export interface PerspectApiV2Config extends PerspectApiConfig {
32
+ cache?: CacheConfig;
33
+ }
34
+
29
35
  export class PerspectApiV2Client {
30
36
  private http: HttpClient;
37
+ readonly cache: CacheManager;
31
38
 
32
39
  readonly content: ContentV2Client;
33
40
  readonly products: ProductsV2Client;
@@ -42,7 +49,7 @@ export class PerspectApiV2Client {
42
49
  readonly apiKeys: ApiKeysV2Client;
43
50
  readonly webhooks: WebhooksV2Client;
44
51
 
45
- constructor(config: PerspectApiConfig) {
52
+ constructor(config: PerspectApiV2Config) {
46
53
  // Ensure base URL points to /api/v2
47
54
  const baseUrl = config.baseUrl.replace(/\/+$/, '');
48
55
  const v2BaseUrl = baseUrl.endsWith('/api/v2')
@@ -50,20 +57,22 @@ export class PerspectApiV2Client {
50
57
  : `${baseUrl}/api/v2`;
51
58
 
52
59
  this.http = new HttpClient({ ...config, baseUrl: v2BaseUrl });
60
+ this.cache = new CacheManager(config.cache);
53
61
 
54
62
  const basePath = '';
55
- this.content = new ContentV2Client(this.http, basePath);
56
- this.products = new ProductsV2Client(this.http, basePath);
57
- this.categories = new CategoriesV2Client(this.http, basePath);
58
- this.collections = new CollectionsV2Client(this.http, basePath);
59
- this.orders = new OrdersV2Client(this.http, basePath);
60
- this.siteUsers = new SiteUsersV2Client(this.http, basePath);
61
- this.newsletter = new NewsletterV2Client(this.http, basePath);
62
- this.contacts = new ContactsV2Client(this.http, basePath);
63
- this.organizations = new OrganizationsV2Client(this.http, basePath);
64
- this.sites = new SitesV2Client(this.http, basePath);
65
- this.apiKeys = new ApiKeysV2Client(this.http, basePath);
66
- this.webhooks = new WebhooksV2Client(this.http, basePath);
63
+ const cache = this.cache;
64
+ this.content = new ContentV2Client(this.http, basePath, cache);
65
+ this.products = new ProductsV2Client(this.http, basePath, cache);
66
+ this.categories = new CategoriesV2Client(this.http, basePath, cache);
67
+ this.collections = new CollectionsV2Client(this.http, basePath, cache);
68
+ this.orders = new OrdersV2Client(this.http, basePath, cache);
69
+ this.siteUsers = new SiteUsersV2Client(this.http, basePath, cache);
70
+ this.newsletter = new NewsletterV2Client(this.http, basePath, cache);
71
+ this.contacts = new ContactsV2Client(this.http, basePath, cache);
72
+ this.organizations = new OrganizationsV2Client(this.http, basePath, cache);
73
+ this.sites = new SitesV2Client(this.http, basePath, cache);
74
+ this.apiKeys = new ApiKeysV2Client(this.http, basePath, cache);
75
+ this.webhooks = new WebhooksV2Client(this.http, basePath, cache);
67
76
  }
68
77
 
69
78
  /** Update the JWT token for authenticated requests. */
package/src/v2/types.ts CHANGED
@@ -311,6 +311,10 @@ export interface V2NewsletterCampaign extends V2Object {
311
311
  updated_at: string | null;
312
312
  }
313
313
 
314
+ export interface V2NewsletterTrackingResponse {
315
+ success: boolean;
316
+ }
317
+
314
318
  // --- Contact ---
315
319
 
316
320
  export interface V2ContactSubmission extends V2Object {