perspectapi-ts-sdk 1.5.2 → 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.
package/dist/index.js CHANGED
@@ -23,13 +23,16 @@ __export(index_exports, {
23
23
  ApiKeysClient: () => ApiKeysClient,
24
24
  AuthClient: () => AuthClient,
25
25
  BaseClient: () => BaseClient,
26
+ CacheManager: () => CacheManager,
26
27
  CategoriesClient: () => CategoriesClient,
27
28
  CheckoutClient: () => CheckoutClient,
28
29
  ContactClient: () => ContactClient,
29
30
  ContentClient: () => ContentClient,
30
31
  DEFAULT_IMAGE_SIZES: () => DEFAULT_IMAGE_SIZES,
31
32
  HttpClient: () => HttpClient,
33
+ InMemoryCacheAdapter: () => InMemoryCacheAdapter,
32
34
  NewsletterClient: () => NewsletterClient,
35
+ NoopCacheAdapter: () => NoopCacheAdapter,
33
36
  OrganizationsClient: () => OrganizationsClient,
34
37
  PerspectApiClient: () => PerspectApiClient,
35
38
  ProductsClient: () => ProductsClient,
@@ -263,13 +266,273 @@ function createApiError(error) {
263
266
  };
264
267
  }
265
268
 
269
+ // src/cache/in-memory-adapter.ts
270
+ var InMemoryCacheAdapter = class {
271
+ store = /* @__PURE__ */ new Map();
272
+ async get(key) {
273
+ const entry = this.store.get(key);
274
+ if (!entry) {
275
+ return void 0;
276
+ }
277
+ if (entry.expiresAt && entry.expiresAt <= Date.now()) {
278
+ this.store.delete(key);
279
+ return void 0;
280
+ }
281
+ return entry.value;
282
+ }
283
+ async set(key, value, options) {
284
+ const expiresAt = options?.ttlSeconds && options.ttlSeconds > 0 ? Date.now() + options.ttlSeconds * 1e3 : void 0;
285
+ this.store.set(key, { value, expiresAt });
286
+ }
287
+ async delete(key) {
288
+ this.store.delete(key);
289
+ }
290
+ async deleteMany(keys) {
291
+ keys.forEach((key) => this.store.delete(key));
292
+ }
293
+ async clear() {
294
+ this.store.clear();
295
+ }
296
+ };
297
+
298
+ // src/cache/noop-adapter.ts
299
+ var NoopCacheAdapter = class {
300
+ async get() {
301
+ return void 0;
302
+ }
303
+ async set() {
304
+ }
305
+ async delete() {
306
+ }
307
+ async deleteMany() {
308
+ }
309
+ async clear() {
310
+ }
311
+ };
312
+
313
+ // src/cache/cache-manager.ts
314
+ var TAG_PREFIX = "__tag__";
315
+ var CacheManager = class {
316
+ adapter;
317
+ defaultTtlSeconds;
318
+ keyPrefix;
319
+ enabled;
320
+ constructor(config) {
321
+ const defaultOptions = {
322
+ defaultTtlSeconds: 300,
323
+ keyPrefix: "perspectapi"
324
+ };
325
+ const mergedConfig = {
326
+ ...defaultOptions,
327
+ ...config
328
+ };
329
+ if (config && config.enabled === false) {
330
+ this.enabled = false;
331
+ this.adapter = new NoopCacheAdapter();
332
+ } else if (config && config.adapter) {
333
+ this.enabled = true;
334
+ this.adapter = config.adapter;
335
+ } else if (config) {
336
+ this.enabled = true;
337
+ this.adapter = new InMemoryCacheAdapter();
338
+ } else {
339
+ this.enabled = false;
340
+ this.adapter = new NoopCacheAdapter();
341
+ }
342
+ this.defaultTtlSeconds = mergedConfig.defaultTtlSeconds ?? 300;
343
+ this.keyPrefix = mergedConfig.keyPrefix ?? "perspectapi";
344
+ }
345
+ isEnabled() {
346
+ return this.enabled;
347
+ }
348
+ getKeyPrefix() {
349
+ return this.keyPrefix;
350
+ }
351
+ buildKey(parts) {
352
+ const normalized = parts.flatMap((part) => this.normalizeKeyPart(part)).filter((part) => part !== void 0 && part !== null && part !== "");
353
+ return normalized.join(":");
354
+ }
355
+ async getOrSet(key, resolveValue, policy) {
356
+ if (!this.enabled || policy?.skipCache) {
357
+ const value2 = await resolveValue();
358
+ if (this.enabled && !policy?.skipCache && policy?.ttlSeconds !== 0) {
359
+ await this.set(key, value2, policy);
360
+ }
361
+ return value2;
362
+ }
363
+ const namespacedKey = this.namespacedKey(key);
364
+ const cachedRaw = await this.adapter.get(namespacedKey);
365
+ if (cachedRaw) {
366
+ const entry = this.deserialize(cachedRaw);
367
+ if (!entry.expiresAt || entry.expiresAt > Date.now()) {
368
+ return entry.value;
369
+ }
370
+ await this.adapter.delete(namespacedKey);
371
+ if (entry.tags?.length) {
372
+ await this.removeKeyFromTags(namespacedKey, entry.tags);
373
+ }
374
+ }
375
+ const value = await resolveValue();
376
+ await this.set(key, value, policy);
377
+ return value;
378
+ }
379
+ async set(key, value, options) {
380
+ if (!this.enabled || options?.ttlSeconds === 0) {
381
+ return;
382
+ }
383
+ const namespacedKey = this.namespacedKey(key);
384
+ const ttlSeconds = options?.ttlSeconds ?? this.defaultTtlSeconds;
385
+ const entry = {
386
+ value,
387
+ expiresAt: ttlSeconds > 0 ? Date.now() + ttlSeconds * 1e3 : void 0,
388
+ tags: options?.tags,
389
+ metadata: options?.metadata
390
+ };
391
+ await this.adapter.set(namespacedKey, this.serialize(entry), {
392
+ ttlSeconds: ttlSeconds > 0 ? ttlSeconds : void 0
393
+ });
394
+ if (options?.tags?.length) {
395
+ await this.registerKeyTags(namespacedKey, options.tags);
396
+ }
397
+ }
398
+ async delete(key) {
399
+ if (!this.enabled) {
400
+ return;
401
+ }
402
+ const namespacedKey = this.namespacedKey(key);
403
+ await this.adapter.delete(namespacedKey);
404
+ }
405
+ async invalidate(options) {
406
+ if (!this.enabled) {
407
+ return;
408
+ }
409
+ if (options.keys?.length) {
410
+ const namespacedKeys = options.keys.map((key) => this.namespacedKey(key));
411
+ if (this.adapter.deleteMany) {
412
+ await this.adapter.deleteMany(namespacedKeys);
413
+ } else {
414
+ await Promise.all(namespacedKeys.map((key) => this.adapter.delete(key)));
415
+ }
416
+ }
417
+ if (options.tags?.length) {
418
+ await Promise.all(
419
+ options.tags.map(async (tag) => {
420
+ const tagKey = this.tagKey(tag);
421
+ const payload = await this.adapter.get(tagKey);
422
+ if (!payload) {
423
+ return;
424
+ }
425
+ const keys = this.deserializeTagSet(payload);
426
+ if (keys.length) {
427
+ if (this.adapter.deleteMany) {
428
+ await this.adapter.deleteMany(keys);
429
+ } else {
430
+ await Promise.all(keys.map((key) => this.adapter.delete(key)));
431
+ }
432
+ }
433
+ await this.adapter.delete(tagKey);
434
+ })
435
+ );
436
+ }
437
+ }
438
+ namespacedKey(key) {
439
+ return `${this.keyPrefix}:${key}`;
440
+ }
441
+ tagKey(tag) {
442
+ return this.namespacedKey(`${TAG_PREFIX}:${tag}`);
443
+ }
444
+ serialize(entry) {
445
+ return JSON.stringify(entry);
446
+ }
447
+ deserialize(payload) {
448
+ try {
449
+ return JSON.parse(payload);
450
+ } catch {
451
+ return { value: payload };
452
+ }
453
+ }
454
+ deserializeTagSet(payload) {
455
+ try {
456
+ const parsed = JSON.parse(payload);
457
+ return Array.isArray(parsed) ? parsed : [];
458
+ } catch {
459
+ return [];
460
+ }
461
+ }
462
+ async registerKeyTags(namespacedKey, tags) {
463
+ await Promise.all(
464
+ tags.map(async (tag) => {
465
+ const tagKey = this.tagKey(tag);
466
+ const existingPayload = await this.adapter.get(tagKey);
467
+ const keys = existingPayload ? this.deserializeTagSet(existingPayload) : [];
468
+ if (!keys.includes(namespacedKey)) {
469
+ keys.push(namespacedKey);
470
+ await this.adapter.set(tagKey, JSON.stringify(keys));
471
+ }
472
+ })
473
+ );
474
+ }
475
+ async removeKeyFromTags(namespacedKey, tags) {
476
+ await Promise.all(
477
+ tags.map(async (tag) => {
478
+ const tagKey = this.tagKey(tag);
479
+ const existingPayload = await this.adapter.get(tagKey);
480
+ if (!existingPayload) {
481
+ return;
482
+ }
483
+ const keys = this.deserializeTagSet(existingPayload);
484
+ const updated = keys.filter((key) => key !== namespacedKey);
485
+ if (updated.length === 0) {
486
+ await this.adapter.delete(tagKey);
487
+ } else if (updated.length !== keys.length) {
488
+ await this.adapter.set(tagKey, JSON.stringify(updated));
489
+ }
490
+ })
491
+ );
492
+ }
493
+ normalizeKeyPart(part) {
494
+ if (part === void 0 || part === null || part === "") {
495
+ return [];
496
+ }
497
+ if (Array.isArray(part)) {
498
+ return part.flatMap((item) => this.normalizeKeyPart(item));
499
+ }
500
+ if (typeof part === "object") {
501
+ return [this.normalizeObject(part)];
502
+ }
503
+ return [String(part)];
504
+ }
505
+ normalizeObject(input) {
506
+ const sortedKeys = Object.keys(input).sort();
507
+ const normalized = {};
508
+ for (const key of sortedKeys) {
509
+ const value = input[key];
510
+ if (value === void 0) {
511
+ continue;
512
+ }
513
+ if (value && typeof value === "object" && !Array.isArray(value)) {
514
+ normalized[key] = JSON.parse(this.normalizeObject(value));
515
+ } else if (Array.isArray(value)) {
516
+ normalized[key] = value.map(
517
+ (item) => typeof item === "object" && item !== null ? JSON.parse(this.normalizeObject(item)) : item
518
+ );
519
+ } else {
520
+ normalized[key] = value;
521
+ }
522
+ }
523
+ return JSON.stringify(normalized);
524
+ }
525
+ };
526
+
266
527
  // src/client/base-client.ts
267
528
  var BaseClient = class {
268
529
  http;
269
530
  basePath;
270
- constructor(http, basePath) {
531
+ cache;
532
+ constructor(http, basePath, cache) {
271
533
  this.http = http;
272
534
  this.basePath = basePath;
535
+ this.cache = cache && cache.isEnabled() ? cache : void 0;
273
536
  }
274
537
  /**
275
538
  * Build a site-scoped endpoint relative to the API base path
@@ -329,12 +592,71 @@ var BaseClient = class {
329
592
  async delete(endpoint, csrfToken) {
330
593
  return this.http.delete(this.buildPath(endpoint), { csrfToken });
331
594
  }
595
+ /**
596
+ * Fetch a GET endpoint with optional caching support.
597
+ */
598
+ async fetchWithCache(endpoint, params, tags, policy, fetcher) {
599
+ if (!this.cache) {
600
+ return fetcher();
601
+ }
602
+ const cacheKey = this.buildCacheKey(endpoint, params);
603
+ const combinedPolicy = policy ? { ...policy, tags: this.mergeTags(tags, policy.tags) } : { tags };
604
+ return this.cache.getOrSet(cacheKey, fetcher, combinedPolicy);
605
+ }
606
+ /**
607
+ * Invalidate cache entries by keys or tags.
608
+ */
609
+ async invalidateCache(options) {
610
+ if (!this.cache) {
611
+ return;
612
+ }
613
+ await this.cache.invalidate(options);
614
+ }
615
+ /**
616
+ * Build a consistent cache key for an endpoint + params combination.
617
+ */
618
+ buildCacheKey(endpoint, params) {
619
+ const sanitizedEndpoint = endpoint.replace(/^\//, "");
620
+ const baseSegment = this.basePath.replace(/^\//, "");
621
+ const parts = [baseSegment, sanitizedEndpoint];
622
+ if (params && Object.keys(params).length > 0) {
623
+ parts.push(this.sortObject(params));
624
+ }
625
+ if (this.cache) {
626
+ return this.cache.buildKey(parts);
627
+ }
628
+ return parts.map((part) => typeof part === "string" ? part : JSON.stringify(part)).join(":");
629
+ }
630
+ mergeTags(defaultTags, overrideTags) {
631
+ const combined = /* @__PURE__ */ new Set();
632
+ defaultTags.forEach((tag) => combined.add(tag));
633
+ overrideTags?.forEach((tag) => combined.add(tag));
634
+ return Array.from(combined.values());
635
+ }
636
+ sortObject(input) {
637
+ const sortedKeys = Object.keys(input).sort();
638
+ const result = {};
639
+ for (const key of sortedKeys) {
640
+ const value = input[key];
641
+ if (value === void 0) continue;
642
+ if (Array.isArray(value)) {
643
+ result[key] = value.map(
644
+ (item) => typeof item === "object" && item !== null ? this.sortObject(item) : item
645
+ );
646
+ } else if (value && typeof value === "object") {
647
+ result[key] = this.sortObject(value);
648
+ } else {
649
+ result[key] = value;
650
+ }
651
+ }
652
+ return result;
653
+ }
332
654
  };
333
655
 
334
656
  // src/client/auth-client.ts
335
657
  var AuthClient = class extends BaseClient {
336
- constructor(http) {
337
- super(http, "/api/v1");
658
+ constructor(http, cache) {
659
+ super(http, "/api/v1", cache);
338
660
  }
339
661
  /**
340
662
  * Get CSRF token
@@ -395,28 +717,50 @@ var AuthClient = class extends BaseClient {
395
717
 
396
718
  // src/client/content-client.ts
397
719
  var ContentClient = class extends BaseClient {
398
- constructor(http) {
399
- super(http, "/api/v1");
720
+ constructor(http, cache) {
721
+ super(http, "/api/v1", cache);
400
722
  }
401
723
  /**
402
724
  * Get all content with pagination and filtering for a site
403
725
  */
404
- async getContent(siteName, params) {
405
- return this.http.get(this.buildPath(this.siteScopedEndpoint(siteName)), params);
726
+ async getContent(siteName, params, cachePolicy) {
727
+ const endpoint = this.siteScopedEndpoint(siteName);
728
+ const path = this.buildPath(endpoint);
729
+ return this.fetchWithCache(
730
+ endpoint,
731
+ params,
732
+ this.buildContentTags(siteName),
733
+ cachePolicy,
734
+ () => this.http.get(path, params)
735
+ );
406
736
  }
407
737
  /**
408
738
  * Get content by ID
409
739
  */
410
- async getContentById(id) {
411
- return this.getSingle(`/content/${id}`);
740
+ async getContentById(id, cachePolicy) {
741
+ const endpoint = `/content/${id}`;
742
+ const path = this.buildPath(endpoint);
743
+ return this.fetchWithCache(
744
+ endpoint,
745
+ void 0,
746
+ this.buildContentTags(void 0, void 0, id),
747
+ cachePolicy,
748
+ () => this.http.get(path)
749
+ );
412
750
  }
413
751
  /**
414
752
  * Get content by slug for a site
415
753
  */
416
- async getContentBySlug(siteName, slug) {
417
- return this.http.get(this.buildPath(
418
- this.siteScopedEndpoint(siteName, `/slug/${encodeURIComponent(slug)}`)
419
- ));
754
+ async getContentBySlug(siteName, slug, cachePolicy) {
755
+ const endpoint = this.siteScopedEndpoint(siteName, `/slug/${encodeURIComponent(slug)}`);
756
+ const path = this.buildPath(endpoint);
757
+ return this.fetchWithCache(
758
+ endpoint,
759
+ void 0,
760
+ this.buildContentTags(siteName, slug),
761
+ cachePolicy,
762
+ () => this.http.get(path)
763
+ );
420
764
  }
421
765
  /**
422
766
  * Create new content
@@ -478,12 +822,25 @@ var ContentClient = class extends BaseClient {
478
822
  async duplicateContent(id) {
479
823
  return this.create(`/content/${id}/duplicate`, {});
480
824
  }
825
+ buildContentTags(siteName, slug, id) {
826
+ const tags = /* @__PURE__ */ new Set(["content"]);
827
+ if (siteName) {
828
+ tags.add(`content:site:${siteName}`);
829
+ }
830
+ if (slug) {
831
+ tags.add(`content:slug:${siteName}:${slug}`);
832
+ }
833
+ if (typeof id === "number") {
834
+ tags.add(`content:id:${id}`);
835
+ }
836
+ return Array.from(tags.values());
837
+ }
481
838
  };
482
839
 
483
840
  // src/client/api-keys-client.ts
484
841
  var ApiKeysClient = class extends BaseClient {
485
- constructor(http) {
486
- super(http, "/api/v1");
842
+ constructor(http, cache) {
843
+ super(http, "/api/v1", cache);
487
844
  }
488
845
  /**
489
846
  * Get all API keys
@@ -549,8 +906,8 @@ var ApiKeysClient = class extends BaseClient {
549
906
 
550
907
  // src/client/organizations-client.ts
551
908
  var OrganizationsClient = class extends BaseClient {
552
- constructor(http) {
553
- super(http, "/api/v1");
909
+ constructor(http, cache) {
910
+ super(http, "/api/v1", cache);
554
911
  }
555
912
  /**
556
913
  * Get all organizations
@@ -622,8 +979,8 @@ var OrganizationsClient = class extends BaseClient {
622
979
 
623
980
  // src/client/sites-client.ts
624
981
  var SitesClient = class extends BaseClient {
625
- constructor(http) {
626
- super(http, "/api/v1");
982
+ constructor(http, cache) {
983
+ super(http, "/api/v1", cache);
627
984
  }
628
985
  /**
629
986
  * Get all sites
@@ -719,13 +1076,13 @@ var SitesClient = class extends BaseClient {
719
1076
 
720
1077
  // src/client/products-client.ts
721
1078
  var ProductsClient = class extends BaseClient {
722
- constructor(http) {
723
- super(http, "/api/v1");
1079
+ constructor(http, cache) {
1080
+ super(http, "/api/v1", cache);
724
1081
  }
725
1082
  /**
726
1083
  * Get all products for a site
727
1084
  */
728
- async getProducts(siteName, params) {
1085
+ async getProducts(siteName, params, cachePolicy) {
729
1086
  const normalizeList = (value) => {
730
1087
  if (value === void 0 || value === null) {
731
1088
  return void 0;
@@ -749,30 +1106,61 @@ var ProductsClient = class extends BaseClient {
749
1106
  delete normalizedParams.category_id;
750
1107
  }
751
1108
  }
752
- return this.http.get(
753
- this.buildPath(this.siteScopedEndpoint(siteName, "/products", { includeSitesSegment: false })),
754
- normalizedParams
1109
+ const endpoint = this.siteScopedEndpoint(siteName, "/products", { includeSitesSegment: false });
1110
+ const path = this.buildPath(endpoint);
1111
+ return this.fetchWithCache(
1112
+ endpoint,
1113
+ normalizedParams,
1114
+ this.buildProductTags(siteName, ["products:list"]),
1115
+ cachePolicy,
1116
+ () => this.http.get(path, normalizedParams)
755
1117
  );
756
1118
  }
757
1119
  /**
758
1120
  * Get product by ID
759
1121
  */
760
- async getProductById(id) {
761
- return this.getSingle(`/products/${id}`);
1122
+ async getProductById(id, cachePolicy) {
1123
+ const endpoint = `/products/${id}`;
1124
+ const path = this.buildPath(endpoint);
1125
+ return this.fetchWithCache(
1126
+ endpoint,
1127
+ void 0,
1128
+ this.buildProductTags(void 0, [`products:id:${id}`]),
1129
+ cachePolicy,
1130
+ () => this.http.get(path)
1131
+ );
762
1132
  }
763
1133
  /**
764
1134
  * Get product by SKU
765
1135
  */
766
- async getProductBySku(sku) {
767
- return this.getSingle(`/products/sku/${sku}`);
1136
+ async getProductBySku(sku, cachePolicy) {
1137
+ const endpoint = `/products/sku/${encodeURIComponent(sku)}`;
1138
+ const path = this.buildPath(endpoint);
1139
+ return this.fetchWithCache(
1140
+ endpoint,
1141
+ void 0,
1142
+ this.buildProductTags(void 0, [`products:sku:${sku.toLowerCase()}`]),
1143
+ cachePolicy,
1144
+ () => this.http.get(path)
1145
+ );
768
1146
  }
769
1147
  /**
770
1148
  * Get product by slug and site name
771
1149
  */
772
- async getProductBySlug(siteName, slug) {
773
- return this.http.get(this.buildPath(
774
- this.siteScopedEndpoint(siteName, `/products/slug/${encodeURIComponent(slug)}`, { includeSitesSegment: false })
775
- ));
1150
+ async getProductBySlug(siteName, slug, cachePolicy) {
1151
+ const endpoint = this.siteScopedEndpoint(
1152
+ siteName,
1153
+ `/products/slug/${encodeURIComponent(slug)}`,
1154
+ { includeSitesSegment: false }
1155
+ );
1156
+ const path = this.buildPath(endpoint);
1157
+ return this.fetchWithCache(
1158
+ endpoint,
1159
+ void 0,
1160
+ this.buildProductTags(siteName, [`products:slug:${siteName}:${slug}`]),
1161
+ cachePolicy,
1162
+ () => this.http.get(path)
1163
+ );
776
1164
  }
777
1165
  /**
778
1166
  * Create new product
@@ -861,27 +1249,44 @@ var ProductsClient = class extends BaseClient {
861
1249
  /**
862
1250
  * Get products by category slug
863
1251
  */
864
- async getProductsByCategorySlug(siteName, categorySlug, params) {
1252
+ async getProductsByCategorySlug(siteName, categorySlug, params, cachePolicy) {
865
1253
  const queryParams = params ? {
866
1254
  limit: params.limit,
867
1255
  offset: params.page ? (params.page - 1) * (params.limit || 20) : void 0,
868
1256
  published: params.published,
869
1257
  search: params.search
870
1258
  } : void 0;
871
- return this.http.get(this.buildPath(
872
- this.siteScopedEndpoint(
873
- siteName,
874
- `/products/category/${encodeURIComponent(categorySlug)}`,
875
- { includeSitesSegment: false }
876
- )
877
- ), queryParams);
1259
+ const endpoint = this.siteScopedEndpoint(
1260
+ siteName,
1261
+ `/products/category/${encodeURIComponent(categorySlug)}`,
1262
+ { includeSitesSegment: false }
1263
+ );
1264
+ const path = this.buildPath(endpoint);
1265
+ return this.fetchWithCache(
1266
+ endpoint,
1267
+ queryParams,
1268
+ this.buildProductTags(siteName, [
1269
+ "products:category",
1270
+ `products:category:${siteName}:${categorySlug}`
1271
+ ]),
1272
+ cachePolicy,
1273
+ () => this.http.get(path, queryParams)
1274
+ );
1275
+ }
1276
+ buildProductTags(siteName, extraTags = []) {
1277
+ const tags = /* @__PURE__ */ new Set(["products"]);
1278
+ if (siteName) {
1279
+ tags.add(`products:site:${siteName}`);
1280
+ }
1281
+ extraTags.filter(Boolean).forEach((tag) => tags.add(tag));
1282
+ return Array.from(tags.values());
878
1283
  }
879
1284
  };
880
1285
 
881
1286
  // src/client/categories-client.ts
882
1287
  var CategoriesClient = class extends BaseClient {
883
- constructor(http) {
884
- super(http, "/api/v1");
1288
+ constructor(http, cache) {
1289
+ super(http, "/api/v1", cache);
885
1290
  }
886
1291
  /**
887
1292
  * Get all categories
@@ -904,14 +1309,20 @@ var CategoriesClient = class extends BaseClient {
904
1309
  /**
905
1310
  * Get product category by slug (for products)
906
1311
  */
907
- async getProductCategoryBySlug(siteName, slug) {
908
- return this.http.get(this.buildPath(
909
- this.siteScopedEndpoint(
910
- siteName,
911
- `/product_category/slug/${encodeURIComponent(slug)}`,
912
- { includeSitesSegment: false }
913
- )
914
- ));
1312
+ async getProductCategoryBySlug(siteName, slug, cachePolicy) {
1313
+ const endpoint = this.siteScopedEndpoint(
1314
+ siteName,
1315
+ `/product_category/slug/${encodeURIComponent(slug)}`,
1316
+ { includeSitesSegment: false }
1317
+ );
1318
+ const path = this.buildPath(endpoint);
1319
+ return this.fetchWithCache(
1320
+ endpoint,
1321
+ void 0,
1322
+ this.buildCategoryTags(siteName, slug),
1323
+ cachePolicy,
1324
+ () => this.http.get(path)
1325
+ );
915
1326
  }
916
1327
  /**
917
1328
  * Create new category
@@ -980,12 +1391,22 @@ var CategoriesClient = class extends BaseClient {
980
1391
  async searchCategories(query, params) {
981
1392
  return this.http.get(`/categories/search`, { q: query, ...params });
982
1393
  }
1394
+ buildCategoryTags(siteName, slug) {
1395
+ const tags = /* @__PURE__ */ new Set(["categories"]);
1396
+ if (siteName) {
1397
+ tags.add(`categories:site:${siteName}`);
1398
+ }
1399
+ if (slug) {
1400
+ tags.add(`categories:product:${siteName}:${slug}`);
1401
+ }
1402
+ return Array.from(tags.values());
1403
+ }
983
1404
  };
984
1405
 
985
1406
  // src/client/webhooks-client.ts
986
1407
  var WebhooksClient = class extends BaseClient {
987
- constructor(http) {
988
- super(http, "/api/v1");
1408
+ constructor(http, cache) {
1409
+ super(http, "/api/v1", cache);
989
1410
  }
990
1411
  /**
991
1412
  * Get all webhooks
@@ -1082,8 +1503,8 @@ var WebhooksClient = class extends BaseClient {
1082
1503
 
1083
1504
  // src/client/checkout-client.ts
1084
1505
  var CheckoutClient = class extends BaseClient {
1085
- constructor(http) {
1086
- super(http, "/api/v1");
1506
+ constructor(http, cache) {
1507
+ super(http, "/api/v1", cache);
1087
1508
  }
1088
1509
  /**
1089
1510
  * Get CSRF token for a specific site
@@ -1194,8 +1615,8 @@ var CheckoutClient = class extends BaseClient {
1194
1615
 
1195
1616
  // src/client/contact-client.ts
1196
1617
  var ContactClient = class extends BaseClient {
1197
- constructor(http) {
1198
- super(http, "/api/v1");
1618
+ constructor(http, cache) {
1619
+ super(http, "/api/v1", cache);
1199
1620
  }
1200
1621
  /**
1201
1622
  * Build a contact endpoint scoped to a site (without /sites prefix)
@@ -1301,8 +1722,8 @@ var ContactClient = class extends BaseClient {
1301
1722
 
1302
1723
  // src/client/newsletter-client.ts
1303
1724
  var NewsletterClient = class extends BaseClient {
1304
- constructor(http) {
1305
- super(http, "/api/v1");
1725
+ constructor(http, cache) {
1726
+ super(http, "/api/v1", cache);
1306
1727
  }
1307
1728
  /**
1308
1729
  * Build a newsletter endpoint scoped to a site (without /sites prefix)
@@ -1497,6 +1918,7 @@ var NewsletterClient = class extends BaseClient {
1497
1918
  // src/perspect-api-client.ts
1498
1919
  var PerspectApiClient = class {
1499
1920
  http;
1921
+ cache;
1500
1922
  // Service clients
1501
1923
  auth;
1502
1924
  content;
@@ -1514,17 +1936,18 @@ var PerspectApiClient = class {
1514
1936
  throw new Error("baseUrl is required in PerspectApiConfig");
1515
1937
  }
1516
1938
  this.http = new HttpClient(config);
1517
- this.auth = new AuthClient(this.http);
1518
- this.content = new ContentClient(this.http);
1519
- this.apiKeys = new ApiKeysClient(this.http);
1520
- this.organizations = new OrganizationsClient(this.http);
1521
- this.sites = new SitesClient(this.http);
1522
- this.products = new ProductsClient(this.http);
1523
- this.categories = new CategoriesClient(this.http);
1524
- this.webhooks = new WebhooksClient(this.http);
1525
- this.checkout = new CheckoutClient(this.http);
1526
- this.contact = new ContactClient(this.http);
1527
- this.newsletter = new NewsletterClient(this.http);
1939
+ this.cache = new CacheManager(config.cache);
1940
+ this.auth = new AuthClient(this.http, this.cache);
1941
+ this.content = new ContentClient(this.http, this.cache);
1942
+ this.apiKeys = new ApiKeysClient(this.http, this.cache);
1943
+ this.organizations = new OrganizationsClient(this.http, this.cache);
1944
+ this.sites = new SitesClient(this.http, this.cache);
1945
+ this.products = new ProductsClient(this.http, this.cache);
1946
+ this.categories = new CategoriesClient(this.http, this.cache);
1947
+ this.webhooks = new WebhooksClient(this.http, this.cache);
1948
+ this.checkout = new CheckoutClient(this.http, this.cache);
1949
+ this.contact = new ContactClient(this.http, this.cache);
1950
+ this.newsletter = new NewsletterClient(this.http, this.cache);
1528
1951
  }
1529
1952
  /**
1530
1953
  * Update authentication token
@@ -2105,13 +2528,16 @@ async function createCheckoutSession(options) {
2105
2528
  ApiKeysClient,
2106
2529
  AuthClient,
2107
2530
  BaseClient,
2531
+ CacheManager,
2108
2532
  CategoriesClient,
2109
2533
  CheckoutClient,
2110
2534
  ContactClient,
2111
2535
  ContentClient,
2112
2536
  DEFAULT_IMAGE_SIZES,
2113
2537
  HttpClient,
2538
+ InMemoryCacheAdapter,
2114
2539
  NewsletterClient,
2540
+ NoopCacheAdapter,
2115
2541
  OrganizationsClient,
2116
2542
  PerspectApiClient,
2117
2543
  ProductsClient,