perspectapi-ts-sdk 1.5.2 → 2.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.
package/dist/index.mjs CHANGED
@@ -205,13 +205,287 @@ function createApiError(error) {
205
205
  };
206
206
  }
207
207
 
208
+ // src/cache/in-memory-adapter.ts
209
+ var InMemoryCacheAdapter = class {
210
+ store = /* @__PURE__ */ new Map();
211
+ async get(key) {
212
+ const entry = this.store.get(key);
213
+ if (!entry) {
214
+ return void 0;
215
+ }
216
+ if (entry.expiresAt && entry.expiresAt <= Date.now()) {
217
+ this.store.delete(key);
218
+ return void 0;
219
+ }
220
+ return entry.value;
221
+ }
222
+ async set(key, value, options) {
223
+ const expiresAt = options?.ttlSeconds && options.ttlSeconds > 0 ? Date.now() + options.ttlSeconds * 1e3 : void 0;
224
+ this.store.set(key, { value, expiresAt });
225
+ }
226
+ async delete(key) {
227
+ this.store.delete(key);
228
+ }
229
+ async deleteMany(keys) {
230
+ keys.forEach((key) => this.store.delete(key));
231
+ }
232
+ async clear() {
233
+ this.store.clear();
234
+ }
235
+ };
236
+
237
+ // src/cache/noop-adapter.ts
238
+ var NoopCacheAdapter = class {
239
+ async get() {
240
+ return void 0;
241
+ }
242
+ async set() {
243
+ }
244
+ async delete() {
245
+ }
246
+ async deleteMany() {
247
+ }
248
+ async clear() {
249
+ }
250
+ };
251
+
252
+ // src/cache/cache-manager.ts
253
+ var TAG_PREFIX = "__tag__";
254
+ var CacheManager = class {
255
+ adapter;
256
+ defaultTtlSeconds;
257
+ keyPrefix;
258
+ enabled;
259
+ constructor(config) {
260
+ const defaultOptions = {
261
+ defaultTtlSeconds: 300,
262
+ keyPrefix: "perspectapi"
263
+ };
264
+ const mergedConfig = {
265
+ ...defaultOptions,
266
+ ...config
267
+ };
268
+ if (config && config.enabled === false) {
269
+ this.enabled = false;
270
+ this.adapter = new NoopCacheAdapter();
271
+ } else if (config && config.adapter) {
272
+ this.enabled = true;
273
+ this.adapter = config.adapter;
274
+ } else if (config) {
275
+ this.enabled = true;
276
+ this.adapter = new InMemoryCacheAdapter();
277
+ } else {
278
+ this.enabled = false;
279
+ this.adapter = new NoopCacheAdapter();
280
+ }
281
+ this.defaultTtlSeconds = mergedConfig.defaultTtlSeconds ?? 300;
282
+ this.keyPrefix = mergedConfig.keyPrefix ?? "perspectapi";
283
+ }
284
+ isEnabled() {
285
+ return this.enabled;
286
+ }
287
+ getKeyPrefix() {
288
+ return this.keyPrefix;
289
+ }
290
+ buildKey(parts) {
291
+ const normalized = parts.flatMap((part) => this.normalizeKeyPart(part)).filter((part) => part !== void 0 && part !== null && part !== "");
292
+ return normalized.join(":");
293
+ }
294
+ async getOrSet(key, resolveValue, policy) {
295
+ if (!this.enabled || policy?.skipCache) {
296
+ console.log("[Cache] Cache disabled or skipped", { key, enabled: this.enabled, skipCache: policy?.skipCache });
297
+ const value2 = await resolveValue();
298
+ if (this.enabled && !policy?.skipCache && policy?.ttlSeconds !== 0) {
299
+ await this.set(key, value2, policy);
300
+ }
301
+ return value2;
302
+ }
303
+ const namespacedKey = this.namespacedKey(key);
304
+ const cachedRaw = await this.adapter.get(namespacedKey);
305
+ if (cachedRaw) {
306
+ const entry = this.deserialize(cachedRaw);
307
+ if (!entry.expiresAt || entry.expiresAt > Date.now()) {
308
+ console.log("[Cache] \u2713 HIT", { key, tags: entry.tags });
309
+ return entry.value;
310
+ }
311
+ console.log("[Cache] \u2717 EXPIRED", { key, expiresAt: new Date(entry.expiresAt) });
312
+ await this.adapter.delete(namespacedKey);
313
+ if (entry.tags?.length) {
314
+ await this.removeKeyFromTags(namespacedKey, entry.tags);
315
+ }
316
+ } else {
317
+ console.log("[Cache] \u2717 MISS", { key });
318
+ }
319
+ const value = await resolveValue();
320
+ await this.set(key, value, policy);
321
+ return value;
322
+ }
323
+ async set(key, value, options) {
324
+ if (!this.enabled || options?.ttlSeconds === 0) {
325
+ return;
326
+ }
327
+ const namespacedKey = this.namespacedKey(key);
328
+ const ttlSeconds = options?.ttlSeconds ?? this.defaultTtlSeconds;
329
+ const entry = {
330
+ value,
331
+ expiresAt: ttlSeconds > 0 ? Date.now() + ttlSeconds * 1e3 : void 0,
332
+ tags: options?.tags,
333
+ metadata: options?.metadata
334
+ };
335
+ console.log("[Cache] SET", { key, ttlSeconds, tags: options?.tags });
336
+ await this.adapter.set(namespacedKey, this.serialize(entry), {
337
+ ttlSeconds: ttlSeconds > 0 ? ttlSeconds : void 0
338
+ });
339
+ if (options?.tags?.length) {
340
+ await this.registerKeyTags(namespacedKey, options.tags);
341
+ }
342
+ }
343
+ async delete(key) {
344
+ if (!this.enabled) {
345
+ return;
346
+ }
347
+ const namespacedKey = this.namespacedKey(key);
348
+ await this.adapter.delete(namespacedKey);
349
+ }
350
+ async invalidate(options) {
351
+ if (!this.enabled) {
352
+ return;
353
+ }
354
+ let totalInvalidated = 0;
355
+ if (options.keys?.length) {
356
+ const namespacedKeys = options.keys.map((key) => this.namespacedKey(key));
357
+ console.log("[Cache] INVALIDATE by keys", { count: options.keys.length, keys: options.keys });
358
+ if (this.adapter.deleteMany) {
359
+ await this.adapter.deleteMany(namespacedKeys);
360
+ } else {
361
+ await Promise.all(namespacedKeys.map((key) => this.adapter.delete(key)));
362
+ }
363
+ totalInvalidated += options.keys.length;
364
+ }
365
+ if (options.tags?.length) {
366
+ console.log("[Cache] INVALIDATE by tags", { tags: options.tags });
367
+ await Promise.all(
368
+ options.tags.map(async (tag) => {
369
+ const tagKey = this.tagKey(tag);
370
+ const payload = await this.adapter.get(tagKey);
371
+ if (!payload) {
372
+ console.log("[Cache] No entries for tag", { tag });
373
+ return;
374
+ }
375
+ const keys = this.deserializeTagSet(payload);
376
+ if (keys.length) {
377
+ console.log("[Cache] Invalidating entries for tag", { tag, count: keys.length });
378
+ if (this.adapter.deleteMany) {
379
+ await this.adapter.deleteMany(keys);
380
+ } else {
381
+ await Promise.all(keys.map((key) => this.adapter.delete(key)));
382
+ }
383
+ totalInvalidated += keys.length;
384
+ }
385
+ await this.adapter.delete(tagKey);
386
+ })
387
+ );
388
+ }
389
+ console.log("[Cache] \u2713 INVALIDATED", { totalEntries: totalInvalidated, keys: options.keys?.length || 0, tags: options.tags?.length || 0 });
390
+ }
391
+ namespacedKey(key) {
392
+ return `${this.keyPrefix}:${key}`;
393
+ }
394
+ tagKey(tag) {
395
+ return this.namespacedKey(`${TAG_PREFIX}:${tag}`);
396
+ }
397
+ serialize(entry) {
398
+ return JSON.stringify(entry);
399
+ }
400
+ deserialize(payload) {
401
+ try {
402
+ return JSON.parse(payload);
403
+ } catch {
404
+ return { value: payload };
405
+ }
406
+ }
407
+ deserializeTagSet(payload) {
408
+ try {
409
+ const parsed = JSON.parse(payload);
410
+ return Array.isArray(parsed) ? parsed : [];
411
+ } catch {
412
+ return [];
413
+ }
414
+ }
415
+ async registerKeyTags(namespacedKey, tags) {
416
+ await Promise.all(
417
+ tags.map(async (tag) => {
418
+ const tagKey = this.tagKey(tag);
419
+ const existingPayload = await this.adapter.get(tagKey);
420
+ const keys = existingPayload ? this.deserializeTagSet(existingPayload) : [];
421
+ if (!keys.includes(namespacedKey)) {
422
+ keys.push(namespacedKey);
423
+ await this.adapter.set(tagKey, JSON.stringify(keys));
424
+ }
425
+ })
426
+ );
427
+ }
428
+ async removeKeyFromTags(namespacedKey, tags) {
429
+ await Promise.all(
430
+ tags.map(async (tag) => {
431
+ const tagKey = this.tagKey(tag);
432
+ const existingPayload = await this.adapter.get(tagKey);
433
+ if (!existingPayload) {
434
+ return;
435
+ }
436
+ const keys = this.deserializeTagSet(existingPayload);
437
+ const updated = keys.filter((key) => key !== namespacedKey);
438
+ if (updated.length === 0) {
439
+ await this.adapter.delete(tagKey);
440
+ } else if (updated.length !== keys.length) {
441
+ await this.adapter.set(tagKey, JSON.stringify(updated));
442
+ }
443
+ })
444
+ );
445
+ }
446
+ normalizeKeyPart(part) {
447
+ if (part === void 0 || part === null || part === "") {
448
+ return [];
449
+ }
450
+ if (Array.isArray(part)) {
451
+ return part.flatMap((item) => this.normalizeKeyPart(item));
452
+ }
453
+ if (typeof part === "object") {
454
+ return [this.normalizeObject(part)];
455
+ }
456
+ return [String(part)];
457
+ }
458
+ normalizeObject(input) {
459
+ const sortedKeys = Object.keys(input).sort();
460
+ const normalized = {};
461
+ for (const key of sortedKeys) {
462
+ const value = input[key];
463
+ if (value === void 0) {
464
+ continue;
465
+ }
466
+ if (value && typeof value === "object" && !Array.isArray(value)) {
467
+ normalized[key] = JSON.parse(this.normalizeObject(value));
468
+ } else if (Array.isArray(value)) {
469
+ normalized[key] = value.map(
470
+ (item) => typeof item === "object" && item !== null ? JSON.parse(this.normalizeObject(item)) : item
471
+ );
472
+ } else {
473
+ normalized[key] = value;
474
+ }
475
+ }
476
+ return JSON.stringify(normalized);
477
+ }
478
+ };
479
+
208
480
  // src/client/base-client.ts
209
481
  var BaseClient = class {
210
482
  http;
211
483
  basePath;
212
- constructor(http, basePath) {
484
+ cache;
485
+ constructor(http, basePath, cache) {
213
486
  this.http = http;
214
487
  this.basePath = basePath;
488
+ this.cache = cache && cache.isEnabled() ? cache : void 0;
215
489
  }
216
490
  /**
217
491
  * Build a site-scoped endpoint relative to the API base path
@@ -271,12 +545,71 @@ var BaseClient = class {
271
545
  async delete(endpoint, csrfToken) {
272
546
  return this.http.delete(this.buildPath(endpoint), { csrfToken });
273
547
  }
548
+ /**
549
+ * Fetch a GET endpoint with optional caching support.
550
+ */
551
+ async fetchWithCache(endpoint, params, tags, policy, fetcher) {
552
+ if (!this.cache) {
553
+ return fetcher();
554
+ }
555
+ const cacheKey = this.buildCacheKey(endpoint, params);
556
+ const combinedPolicy = policy ? { ...policy, tags: this.mergeTags(tags, policy.tags) } : { tags };
557
+ return this.cache.getOrSet(cacheKey, fetcher, combinedPolicy);
558
+ }
559
+ /**
560
+ * Invalidate cache entries by keys or tags.
561
+ */
562
+ async invalidateCache(options) {
563
+ if (!this.cache) {
564
+ return;
565
+ }
566
+ await this.cache.invalidate(options);
567
+ }
568
+ /**
569
+ * Build a consistent cache key for an endpoint + params combination.
570
+ */
571
+ buildCacheKey(endpoint, params) {
572
+ const sanitizedEndpoint = endpoint.replace(/^\//, "");
573
+ const baseSegment = this.basePath.replace(/^\//, "");
574
+ const parts = [baseSegment, sanitizedEndpoint];
575
+ if (params && Object.keys(params).length > 0) {
576
+ parts.push(this.sortObject(params));
577
+ }
578
+ if (this.cache) {
579
+ return this.cache.buildKey(parts);
580
+ }
581
+ return parts.map((part) => typeof part === "string" ? part : JSON.stringify(part)).join(":");
582
+ }
583
+ mergeTags(defaultTags, overrideTags) {
584
+ const combined = /* @__PURE__ */ new Set();
585
+ defaultTags.forEach((tag) => combined.add(tag));
586
+ overrideTags?.forEach((tag) => combined.add(tag));
587
+ return Array.from(combined.values());
588
+ }
589
+ sortObject(input) {
590
+ const sortedKeys = Object.keys(input).sort();
591
+ const result = {};
592
+ for (const key of sortedKeys) {
593
+ const value = input[key];
594
+ if (value === void 0) continue;
595
+ if (Array.isArray(value)) {
596
+ result[key] = value.map(
597
+ (item) => typeof item === "object" && item !== null ? this.sortObject(item) : item
598
+ );
599
+ } else if (value && typeof value === "object") {
600
+ result[key] = this.sortObject(value);
601
+ } else {
602
+ result[key] = value;
603
+ }
604
+ }
605
+ return result;
606
+ }
274
607
  };
275
608
 
276
609
  // src/client/auth-client.ts
277
610
  var AuthClient = class extends BaseClient {
278
- constructor(http) {
279
- super(http, "/api/v1");
611
+ constructor(http, cache) {
612
+ super(http, "/api/v1", cache);
280
613
  }
281
614
  /**
282
615
  * Get CSRF token
@@ -337,28 +670,50 @@ var AuthClient = class extends BaseClient {
337
670
 
338
671
  // src/client/content-client.ts
339
672
  var ContentClient = class extends BaseClient {
340
- constructor(http) {
341
- super(http, "/api/v1");
673
+ constructor(http, cache) {
674
+ super(http, "/api/v1", cache);
342
675
  }
343
676
  /**
344
677
  * Get all content with pagination and filtering for a site
345
678
  */
346
- async getContent(siteName, params) {
347
- return this.http.get(this.buildPath(this.siteScopedEndpoint(siteName)), params);
679
+ async getContent(siteName, params, cachePolicy) {
680
+ const endpoint = this.siteScopedEndpoint(siteName);
681
+ const path = this.buildPath(endpoint);
682
+ return this.fetchWithCache(
683
+ endpoint,
684
+ params,
685
+ this.buildContentTags(siteName),
686
+ cachePolicy,
687
+ () => this.http.get(path, params)
688
+ );
348
689
  }
349
690
  /**
350
691
  * Get content by ID
351
692
  */
352
- async getContentById(id) {
353
- return this.getSingle(`/content/${id}`);
693
+ async getContentById(id, cachePolicy) {
694
+ const endpoint = `/content/${id}`;
695
+ const path = this.buildPath(endpoint);
696
+ return this.fetchWithCache(
697
+ endpoint,
698
+ void 0,
699
+ this.buildContentTags(void 0, void 0, id),
700
+ cachePolicy,
701
+ () => this.http.get(path)
702
+ );
354
703
  }
355
704
  /**
356
705
  * Get content by slug for a site
357
706
  */
358
- async getContentBySlug(siteName, slug) {
359
- return this.http.get(this.buildPath(
360
- this.siteScopedEndpoint(siteName, `/slug/${encodeURIComponent(slug)}`)
361
- ));
707
+ async getContentBySlug(siteName, slug, cachePolicy) {
708
+ const endpoint = this.siteScopedEndpoint(siteName, `/slug/${encodeURIComponent(slug)}`);
709
+ const path = this.buildPath(endpoint);
710
+ return this.fetchWithCache(
711
+ endpoint,
712
+ void 0,
713
+ this.buildContentTags(siteName, slug),
714
+ cachePolicy,
715
+ () => this.http.get(path)
716
+ );
362
717
  }
363
718
  /**
364
719
  * Create new content
@@ -420,12 +775,25 @@ var ContentClient = class extends BaseClient {
420
775
  async duplicateContent(id) {
421
776
  return this.create(`/content/${id}/duplicate`, {});
422
777
  }
778
+ buildContentTags(siteName, slug, id) {
779
+ const tags = /* @__PURE__ */ new Set(["content"]);
780
+ if (siteName) {
781
+ tags.add(`content:site:${siteName}`);
782
+ }
783
+ if (slug) {
784
+ tags.add(`content:slug:${siteName}:${slug}`);
785
+ }
786
+ if (typeof id === "number") {
787
+ tags.add(`content:id:${id}`);
788
+ }
789
+ return Array.from(tags.values());
790
+ }
423
791
  };
424
792
 
425
793
  // src/client/api-keys-client.ts
426
794
  var ApiKeysClient = class extends BaseClient {
427
- constructor(http) {
428
- super(http, "/api/v1");
795
+ constructor(http, cache) {
796
+ super(http, "/api/v1", cache);
429
797
  }
430
798
  /**
431
799
  * Get all API keys
@@ -491,8 +859,8 @@ var ApiKeysClient = class extends BaseClient {
491
859
 
492
860
  // src/client/organizations-client.ts
493
861
  var OrganizationsClient = class extends BaseClient {
494
- constructor(http) {
495
- super(http, "/api/v1");
862
+ constructor(http, cache) {
863
+ super(http, "/api/v1", cache);
496
864
  }
497
865
  /**
498
866
  * Get all organizations
@@ -564,8 +932,8 @@ var OrganizationsClient = class extends BaseClient {
564
932
 
565
933
  // src/client/sites-client.ts
566
934
  var SitesClient = class extends BaseClient {
567
- constructor(http) {
568
- super(http, "/api/v1");
935
+ constructor(http, cache) {
936
+ super(http, "/api/v1", cache);
569
937
  }
570
938
  /**
571
939
  * Get all sites
@@ -661,13 +1029,13 @@ var SitesClient = class extends BaseClient {
661
1029
 
662
1030
  // src/client/products-client.ts
663
1031
  var ProductsClient = class extends BaseClient {
664
- constructor(http) {
665
- super(http, "/api/v1");
1032
+ constructor(http, cache) {
1033
+ super(http, "/api/v1", cache);
666
1034
  }
667
1035
  /**
668
1036
  * Get all products for a site
669
1037
  */
670
- async getProducts(siteName, params) {
1038
+ async getProducts(siteName, params, cachePolicy) {
671
1039
  const normalizeList = (value) => {
672
1040
  if (value === void 0 || value === null) {
673
1041
  return void 0;
@@ -691,30 +1059,61 @@ var ProductsClient = class extends BaseClient {
691
1059
  delete normalizedParams.category_id;
692
1060
  }
693
1061
  }
694
- return this.http.get(
695
- this.buildPath(this.siteScopedEndpoint(siteName, "/products", { includeSitesSegment: false })),
696
- normalizedParams
1062
+ const endpoint = this.siteScopedEndpoint(siteName, "/products", { includeSitesSegment: false });
1063
+ const path = this.buildPath(endpoint);
1064
+ return this.fetchWithCache(
1065
+ endpoint,
1066
+ normalizedParams,
1067
+ this.buildProductTags(siteName, ["products:list"]),
1068
+ cachePolicy,
1069
+ () => this.http.get(path, normalizedParams)
697
1070
  );
698
1071
  }
699
1072
  /**
700
1073
  * Get product by ID
701
1074
  */
702
- async getProductById(id) {
703
- return this.getSingle(`/products/${id}`);
1075
+ async getProductById(id, cachePolicy) {
1076
+ const endpoint = `/products/${id}`;
1077
+ const path = this.buildPath(endpoint);
1078
+ return this.fetchWithCache(
1079
+ endpoint,
1080
+ void 0,
1081
+ this.buildProductTags(void 0, [`products:id:${id}`]),
1082
+ cachePolicy,
1083
+ () => this.http.get(path)
1084
+ );
704
1085
  }
705
1086
  /**
706
1087
  * Get product by SKU
707
1088
  */
708
- async getProductBySku(sku) {
709
- return this.getSingle(`/products/sku/${sku}`);
1089
+ async getProductBySku(sku, cachePolicy) {
1090
+ const endpoint = `/products/sku/${encodeURIComponent(sku)}`;
1091
+ const path = this.buildPath(endpoint);
1092
+ return this.fetchWithCache(
1093
+ endpoint,
1094
+ void 0,
1095
+ this.buildProductTags(void 0, [`products:sku:${sku.toLowerCase()}`]),
1096
+ cachePolicy,
1097
+ () => this.http.get(path)
1098
+ );
710
1099
  }
711
1100
  /**
712
1101
  * Get product by slug and site name
713
1102
  */
714
- async getProductBySlug(siteName, slug) {
715
- return this.http.get(this.buildPath(
716
- this.siteScopedEndpoint(siteName, `/products/slug/${encodeURIComponent(slug)}`, { includeSitesSegment: false })
717
- ));
1103
+ async getProductBySlug(siteName, slug, cachePolicy) {
1104
+ const endpoint = this.siteScopedEndpoint(
1105
+ siteName,
1106
+ `/products/slug/${encodeURIComponent(slug)}`,
1107
+ { includeSitesSegment: false }
1108
+ );
1109
+ const path = this.buildPath(endpoint);
1110
+ return this.fetchWithCache(
1111
+ endpoint,
1112
+ void 0,
1113
+ this.buildProductTags(siteName, [`products:slug:${siteName}:${slug}`]),
1114
+ cachePolicy,
1115
+ () => this.http.get(path)
1116
+ );
718
1117
  }
719
1118
  /**
720
1119
  * Create new product
@@ -803,27 +1202,44 @@ var ProductsClient = class extends BaseClient {
803
1202
  /**
804
1203
  * Get products by category slug
805
1204
  */
806
- async getProductsByCategorySlug(siteName, categorySlug, params) {
1205
+ async getProductsByCategorySlug(siteName, categorySlug, params, cachePolicy) {
807
1206
  const queryParams = params ? {
808
1207
  limit: params.limit,
809
1208
  offset: params.page ? (params.page - 1) * (params.limit || 20) : void 0,
810
1209
  published: params.published,
811
1210
  search: params.search
812
1211
  } : void 0;
813
- return this.http.get(this.buildPath(
814
- this.siteScopedEndpoint(
815
- siteName,
816
- `/products/category/${encodeURIComponent(categorySlug)}`,
817
- { includeSitesSegment: false }
818
- )
819
- ), queryParams);
1212
+ const endpoint = this.siteScopedEndpoint(
1213
+ siteName,
1214
+ `/products/category/${encodeURIComponent(categorySlug)}`,
1215
+ { includeSitesSegment: false }
1216
+ );
1217
+ const path = this.buildPath(endpoint);
1218
+ return this.fetchWithCache(
1219
+ endpoint,
1220
+ queryParams,
1221
+ this.buildProductTags(siteName, [
1222
+ "products:category",
1223
+ `products:category:${siteName}:${categorySlug}`
1224
+ ]),
1225
+ cachePolicy,
1226
+ () => this.http.get(path, queryParams)
1227
+ );
1228
+ }
1229
+ buildProductTags(siteName, extraTags = []) {
1230
+ const tags = /* @__PURE__ */ new Set(["products"]);
1231
+ if (siteName) {
1232
+ tags.add(`products:site:${siteName}`);
1233
+ }
1234
+ extraTags.filter(Boolean).forEach((tag) => tags.add(tag));
1235
+ return Array.from(tags.values());
820
1236
  }
821
1237
  };
822
1238
 
823
1239
  // src/client/categories-client.ts
824
1240
  var CategoriesClient = class extends BaseClient {
825
- constructor(http) {
826
- super(http, "/api/v1");
1241
+ constructor(http, cache) {
1242
+ super(http, "/api/v1", cache);
827
1243
  }
828
1244
  /**
829
1245
  * Get all categories
@@ -846,14 +1262,20 @@ var CategoriesClient = class extends BaseClient {
846
1262
  /**
847
1263
  * Get product category by slug (for products)
848
1264
  */
849
- async getProductCategoryBySlug(siteName, slug) {
850
- return this.http.get(this.buildPath(
851
- this.siteScopedEndpoint(
852
- siteName,
853
- `/product_category/slug/${encodeURIComponent(slug)}`,
854
- { includeSitesSegment: false }
855
- )
856
- ));
1265
+ async getProductCategoryBySlug(siteName, slug, cachePolicy) {
1266
+ const endpoint = this.siteScopedEndpoint(
1267
+ siteName,
1268
+ `/product_category/slug/${encodeURIComponent(slug)}`,
1269
+ { includeSitesSegment: false }
1270
+ );
1271
+ const path = this.buildPath(endpoint);
1272
+ return this.fetchWithCache(
1273
+ endpoint,
1274
+ void 0,
1275
+ this.buildCategoryTags(siteName, slug),
1276
+ cachePolicy,
1277
+ () => this.http.get(path)
1278
+ );
857
1279
  }
858
1280
  /**
859
1281
  * Create new category
@@ -922,12 +1344,22 @@ var CategoriesClient = class extends BaseClient {
922
1344
  async searchCategories(query, params) {
923
1345
  return this.http.get(`/categories/search`, { q: query, ...params });
924
1346
  }
1347
+ buildCategoryTags(siteName, slug) {
1348
+ const tags = /* @__PURE__ */ new Set(["categories"]);
1349
+ if (siteName) {
1350
+ tags.add(`categories:site:${siteName}`);
1351
+ }
1352
+ if (slug) {
1353
+ tags.add(`categories:product:${siteName}:${slug}`);
1354
+ }
1355
+ return Array.from(tags.values());
1356
+ }
925
1357
  };
926
1358
 
927
1359
  // src/client/webhooks-client.ts
928
1360
  var WebhooksClient = class extends BaseClient {
929
- constructor(http) {
930
- super(http, "/api/v1");
1361
+ constructor(http, cache) {
1362
+ super(http, "/api/v1", cache);
931
1363
  }
932
1364
  /**
933
1365
  * Get all webhooks
@@ -1024,8 +1456,8 @@ var WebhooksClient = class extends BaseClient {
1024
1456
 
1025
1457
  // src/client/checkout-client.ts
1026
1458
  var CheckoutClient = class extends BaseClient {
1027
- constructor(http) {
1028
- super(http, "/api/v1");
1459
+ constructor(http, cache) {
1460
+ super(http, "/api/v1", cache);
1029
1461
  }
1030
1462
  /**
1031
1463
  * Get CSRF token for a specific site
@@ -1136,8 +1568,8 @@ var CheckoutClient = class extends BaseClient {
1136
1568
 
1137
1569
  // src/client/contact-client.ts
1138
1570
  var ContactClient = class extends BaseClient {
1139
- constructor(http) {
1140
- super(http, "/api/v1");
1571
+ constructor(http, cache) {
1572
+ super(http, "/api/v1", cache);
1141
1573
  }
1142
1574
  /**
1143
1575
  * Build a contact endpoint scoped to a site (without /sites prefix)
@@ -1243,8 +1675,8 @@ var ContactClient = class extends BaseClient {
1243
1675
 
1244
1676
  // src/client/newsletter-client.ts
1245
1677
  var NewsletterClient = class extends BaseClient {
1246
- constructor(http) {
1247
- super(http, "/api/v1");
1678
+ constructor(http, cache) {
1679
+ super(http, "/api/v1", cache);
1248
1680
  }
1249
1681
  /**
1250
1682
  * Build a newsletter endpoint scoped to a site (without /sites prefix)
@@ -1439,6 +1871,7 @@ var NewsletterClient = class extends BaseClient {
1439
1871
  // src/perspect-api-client.ts
1440
1872
  var PerspectApiClient = class {
1441
1873
  http;
1874
+ cache;
1442
1875
  // Service clients
1443
1876
  auth;
1444
1877
  content;
@@ -1456,17 +1889,18 @@ var PerspectApiClient = class {
1456
1889
  throw new Error("baseUrl is required in PerspectApiConfig");
1457
1890
  }
1458
1891
  this.http = new HttpClient(config);
1459
- this.auth = new AuthClient(this.http);
1460
- this.content = new ContentClient(this.http);
1461
- this.apiKeys = new ApiKeysClient(this.http);
1462
- this.organizations = new OrganizationsClient(this.http);
1463
- this.sites = new SitesClient(this.http);
1464
- this.products = new ProductsClient(this.http);
1465
- this.categories = new CategoriesClient(this.http);
1466
- this.webhooks = new WebhooksClient(this.http);
1467
- this.checkout = new CheckoutClient(this.http);
1468
- this.contact = new ContactClient(this.http);
1469
- this.newsletter = new NewsletterClient(this.http);
1892
+ this.cache = new CacheManager(config.cache);
1893
+ this.auth = new AuthClient(this.http, this.cache);
1894
+ this.content = new ContentClient(this.http, this.cache);
1895
+ this.apiKeys = new ApiKeysClient(this.http, this.cache);
1896
+ this.organizations = new OrganizationsClient(this.http, this.cache);
1897
+ this.sites = new SitesClient(this.http, this.cache);
1898
+ this.products = new ProductsClient(this.http, this.cache);
1899
+ this.categories = new CategoriesClient(this.http, this.cache);
1900
+ this.webhooks = new WebhooksClient(this.http, this.cache);
1901
+ this.checkout = new CheckoutClient(this.http, this.cache);
1902
+ this.contact = new ContactClient(this.http, this.cache);
1903
+ this.newsletter = new NewsletterClient(this.http, this.cache);
1470
1904
  }
1471
1905
  /**
1472
1906
  * Update authentication token
@@ -2046,13 +2480,16 @@ export {
2046
2480
  ApiKeysClient,
2047
2481
  AuthClient,
2048
2482
  BaseClient,
2483
+ CacheManager,
2049
2484
  CategoriesClient,
2050
2485
  CheckoutClient,
2051
2486
  ContactClient,
2052
2487
  ContentClient,
2053
2488
  DEFAULT_IMAGE_SIZES,
2054
2489
  HttpClient,
2490
+ InMemoryCacheAdapter,
2055
2491
  NewsletterClient,
2492
+ NoopCacheAdapter,
2056
2493
  OrganizationsClient,
2057
2494
  PerspectApiClient,
2058
2495
  ProductsClient,