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.
package/dist/index.mjs CHANGED
@@ -205,13 +205,273 @@ 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
+ const value2 = await resolveValue();
297
+ if (this.enabled && !policy?.skipCache && policy?.ttlSeconds !== 0) {
298
+ await this.set(key, value2, policy);
299
+ }
300
+ return value2;
301
+ }
302
+ const namespacedKey = this.namespacedKey(key);
303
+ const cachedRaw = await this.adapter.get(namespacedKey);
304
+ if (cachedRaw) {
305
+ const entry = this.deserialize(cachedRaw);
306
+ if (!entry.expiresAt || entry.expiresAt > Date.now()) {
307
+ return entry.value;
308
+ }
309
+ await this.adapter.delete(namespacedKey);
310
+ if (entry.tags?.length) {
311
+ await this.removeKeyFromTags(namespacedKey, entry.tags);
312
+ }
313
+ }
314
+ const value = await resolveValue();
315
+ await this.set(key, value, policy);
316
+ return value;
317
+ }
318
+ async set(key, value, options) {
319
+ if (!this.enabled || options?.ttlSeconds === 0) {
320
+ return;
321
+ }
322
+ const namespacedKey = this.namespacedKey(key);
323
+ const ttlSeconds = options?.ttlSeconds ?? this.defaultTtlSeconds;
324
+ const entry = {
325
+ value,
326
+ expiresAt: ttlSeconds > 0 ? Date.now() + ttlSeconds * 1e3 : void 0,
327
+ tags: options?.tags,
328
+ metadata: options?.metadata
329
+ };
330
+ await this.adapter.set(namespacedKey, this.serialize(entry), {
331
+ ttlSeconds: ttlSeconds > 0 ? ttlSeconds : void 0
332
+ });
333
+ if (options?.tags?.length) {
334
+ await this.registerKeyTags(namespacedKey, options.tags);
335
+ }
336
+ }
337
+ async delete(key) {
338
+ if (!this.enabled) {
339
+ return;
340
+ }
341
+ const namespacedKey = this.namespacedKey(key);
342
+ await this.adapter.delete(namespacedKey);
343
+ }
344
+ async invalidate(options) {
345
+ if (!this.enabled) {
346
+ return;
347
+ }
348
+ if (options.keys?.length) {
349
+ const namespacedKeys = options.keys.map((key) => this.namespacedKey(key));
350
+ if (this.adapter.deleteMany) {
351
+ await this.adapter.deleteMany(namespacedKeys);
352
+ } else {
353
+ await Promise.all(namespacedKeys.map((key) => this.adapter.delete(key)));
354
+ }
355
+ }
356
+ if (options.tags?.length) {
357
+ await Promise.all(
358
+ options.tags.map(async (tag) => {
359
+ const tagKey = this.tagKey(tag);
360
+ const payload = await this.adapter.get(tagKey);
361
+ if (!payload) {
362
+ return;
363
+ }
364
+ const keys = this.deserializeTagSet(payload);
365
+ if (keys.length) {
366
+ if (this.adapter.deleteMany) {
367
+ await this.adapter.deleteMany(keys);
368
+ } else {
369
+ await Promise.all(keys.map((key) => this.adapter.delete(key)));
370
+ }
371
+ }
372
+ await this.adapter.delete(tagKey);
373
+ })
374
+ );
375
+ }
376
+ }
377
+ namespacedKey(key) {
378
+ return `${this.keyPrefix}:${key}`;
379
+ }
380
+ tagKey(tag) {
381
+ return this.namespacedKey(`${TAG_PREFIX}:${tag}`);
382
+ }
383
+ serialize(entry) {
384
+ return JSON.stringify(entry);
385
+ }
386
+ deserialize(payload) {
387
+ try {
388
+ return JSON.parse(payload);
389
+ } catch {
390
+ return { value: payload };
391
+ }
392
+ }
393
+ deserializeTagSet(payload) {
394
+ try {
395
+ const parsed = JSON.parse(payload);
396
+ return Array.isArray(parsed) ? parsed : [];
397
+ } catch {
398
+ return [];
399
+ }
400
+ }
401
+ async registerKeyTags(namespacedKey, tags) {
402
+ await Promise.all(
403
+ tags.map(async (tag) => {
404
+ const tagKey = this.tagKey(tag);
405
+ const existingPayload = await this.adapter.get(tagKey);
406
+ const keys = existingPayload ? this.deserializeTagSet(existingPayload) : [];
407
+ if (!keys.includes(namespacedKey)) {
408
+ keys.push(namespacedKey);
409
+ await this.adapter.set(tagKey, JSON.stringify(keys));
410
+ }
411
+ })
412
+ );
413
+ }
414
+ async removeKeyFromTags(namespacedKey, tags) {
415
+ await Promise.all(
416
+ tags.map(async (tag) => {
417
+ const tagKey = this.tagKey(tag);
418
+ const existingPayload = await this.adapter.get(tagKey);
419
+ if (!existingPayload) {
420
+ return;
421
+ }
422
+ const keys = this.deserializeTagSet(existingPayload);
423
+ const updated = keys.filter((key) => key !== namespacedKey);
424
+ if (updated.length === 0) {
425
+ await this.adapter.delete(tagKey);
426
+ } else if (updated.length !== keys.length) {
427
+ await this.adapter.set(tagKey, JSON.stringify(updated));
428
+ }
429
+ })
430
+ );
431
+ }
432
+ normalizeKeyPart(part) {
433
+ if (part === void 0 || part === null || part === "") {
434
+ return [];
435
+ }
436
+ if (Array.isArray(part)) {
437
+ return part.flatMap((item) => this.normalizeKeyPart(item));
438
+ }
439
+ if (typeof part === "object") {
440
+ return [this.normalizeObject(part)];
441
+ }
442
+ return [String(part)];
443
+ }
444
+ normalizeObject(input) {
445
+ const sortedKeys = Object.keys(input).sort();
446
+ const normalized = {};
447
+ for (const key of sortedKeys) {
448
+ const value = input[key];
449
+ if (value === void 0) {
450
+ continue;
451
+ }
452
+ if (value && typeof value === "object" && !Array.isArray(value)) {
453
+ normalized[key] = JSON.parse(this.normalizeObject(value));
454
+ } else if (Array.isArray(value)) {
455
+ normalized[key] = value.map(
456
+ (item) => typeof item === "object" && item !== null ? JSON.parse(this.normalizeObject(item)) : item
457
+ );
458
+ } else {
459
+ normalized[key] = value;
460
+ }
461
+ }
462
+ return JSON.stringify(normalized);
463
+ }
464
+ };
465
+
208
466
  // src/client/base-client.ts
209
467
  var BaseClient = class {
210
468
  http;
211
469
  basePath;
212
- constructor(http, basePath) {
470
+ cache;
471
+ constructor(http, basePath, cache) {
213
472
  this.http = http;
214
473
  this.basePath = basePath;
474
+ this.cache = cache && cache.isEnabled() ? cache : void 0;
215
475
  }
216
476
  /**
217
477
  * Build a site-scoped endpoint relative to the API base path
@@ -271,12 +531,71 @@ var BaseClient = class {
271
531
  async delete(endpoint, csrfToken) {
272
532
  return this.http.delete(this.buildPath(endpoint), { csrfToken });
273
533
  }
534
+ /**
535
+ * Fetch a GET endpoint with optional caching support.
536
+ */
537
+ async fetchWithCache(endpoint, params, tags, policy, fetcher) {
538
+ if (!this.cache) {
539
+ return fetcher();
540
+ }
541
+ const cacheKey = this.buildCacheKey(endpoint, params);
542
+ const combinedPolicy = policy ? { ...policy, tags: this.mergeTags(tags, policy.tags) } : { tags };
543
+ return this.cache.getOrSet(cacheKey, fetcher, combinedPolicy);
544
+ }
545
+ /**
546
+ * Invalidate cache entries by keys or tags.
547
+ */
548
+ async invalidateCache(options) {
549
+ if (!this.cache) {
550
+ return;
551
+ }
552
+ await this.cache.invalidate(options);
553
+ }
554
+ /**
555
+ * Build a consistent cache key for an endpoint + params combination.
556
+ */
557
+ buildCacheKey(endpoint, params) {
558
+ const sanitizedEndpoint = endpoint.replace(/^\//, "");
559
+ const baseSegment = this.basePath.replace(/^\//, "");
560
+ const parts = [baseSegment, sanitizedEndpoint];
561
+ if (params && Object.keys(params).length > 0) {
562
+ parts.push(this.sortObject(params));
563
+ }
564
+ if (this.cache) {
565
+ return this.cache.buildKey(parts);
566
+ }
567
+ return parts.map((part) => typeof part === "string" ? part : JSON.stringify(part)).join(":");
568
+ }
569
+ mergeTags(defaultTags, overrideTags) {
570
+ const combined = /* @__PURE__ */ new Set();
571
+ defaultTags.forEach((tag) => combined.add(tag));
572
+ overrideTags?.forEach((tag) => combined.add(tag));
573
+ return Array.from(combined.values());
574
+ }
575
+ sortObject(input) {
576
+ const sortedKeys = Object.keys(input).sort();
577
+ const result = {};
578
+ for (const key of sortedKeys) {
579
+ const value = input[key];
580
+ if (value === void 0) continue;
581
+ if (Array.isArray(value)) {
582
+ result[key] = value.map(
583
+ (item) => typeof item === "object" && item !== null ? this.sortObject(item) : item
584
+ );
585
+ } else if (value && typeof value === "object") {
586
+ result[key] = this.sortObject(value);
587
+ } else {
588
+ result[key] = value;
589
+ }
590
+ }
591
+ return result;
592
+ }
274
593
  };
275
594
 
276
595
  // src/client/auth-client.ts
277
596
  var AuthClient = class extends BaseClient {
278
- constructor(http) {
279
- super(http, "/api/v1");
597
+ constructor(http, cache) {
598
+ super(http, "/api/v1", cache);
280
599
  }
281
600
  /**
282
601
  * Get CSRF token
@@ -337,28 +656,50 @@ var AuthClient = class extends BaseClient {
337
656
 
338
657
  // src/client/content-client.ts
339
658
  var ContentClient = class extends BaseClient {
340
- constructor(http) {
341
- super(http, "/api/v1");
659
+ constructor(http, cache) {
660
+ super(http, "/api/v1", cache);
342
661
  }
343
662
  /**
344
663
  * Get all content with pagination and filtering for a site
345
664
  */
346
- async getContent(siteName, params) {
347
- return this.http.get(this.buildPath(this.siteScopedEndpoint(siteName)), params);
665
+ async getContent(siteName, params, cachePolicy) {
666
+ const endpoint = this.siteScopedEndpoint(siteName);
667
+ const path = this.buildPath(endpoint);
668
+ return this.fetchWithCache(
669
+ endpoint,
670
+ params,
671
+ this.buildContentTags(siteName),
672
+ cachePolicy,
673
+ () => this.http.get(path, params)
674
+ );
348
675
  }
349
676
  /**
350
677
  * Get content by ID
351
678
  */
352
- async getContentById(id) {
353
- return this.getSingle(`/content/${id}`);
679
+ async getContentById(id, cachePolicy) {
680
+ const endpoint = `/content/${id}`;
681
+ const path = this.buildPath(endpoint);
682
+ return this.fetchWithCache(
683
+ endpoint,
684
+ void 0,
685
+ this.buildContentTags(void 0, void 0, id),
686
+ cachePolicy,
687
+ () => this.http.get(path)
688
+ );
354
689
  }
355
690
  /**
356
691
  * Get content by slug for a site
357
692
  */
358
- async getContentBySlug(siteName, slug) {
359
- return this.http.get(this.buildPath(
360
- this.siteScopedEndpoint(siteName, `/slug/${encodeURIComponent(slug)}`)
361
- ));
693
+ async getContentBySlug(siteName, slug, cachePolicy) {
694
+ const endpoint = this.siteScopedEndpoint(siteName, `/slug/${encodeURIComponent(slug)}`);
695
+ const path = this.buildPath(endpoint);
696
+ return this.fetchWithCache(
697
+ endpoint,
698
+ void 0,
699
+ this.buildContentTags(siteName, slug),
700
+ cachePolicy,
701
+ () => this.http.get(path)
702
+ );
362
703
  }
363
704
  /**
364
705
  * Create new content
@@ -420,12 +761,25 @@ var ContentClient = class extends BaseClient {
420
761
  async duplicateContent(id) {
421
762
  return this.create(`/content/${id}/duplicate`, {});
422
763
  }
764
+ buildContentTags(siteName, slug, id) {
765
+ const tags = /* @__PURE__ */ new Set(["content"]);
766
+ if (siteName) {
767
+ tags.add(`content:site:${siteName}`);
768
+ }
769
+ if (slug) {
770
+ tags.add(`content:slug:${siteName}:${slug}`);
771
+ }
772
+ if (typeof id === "number") {
773
+ tags.add(`content:id:${id}`);
774
+ }
775
+ return Array.from(tags.values());
776
+ }
423
777
  };
424
778
 
425
779
  // src/client/api-keys-client.ts
426
780
  var ApiKeysClient = class extends BaseClient {
427
- constructor(http) {
428
- super(http, "/api/v1");
781
+ constructor(http, cache) {
782
+ super(http, "/api/v1", cache);
429
783
  }
430
784
  /**
431
785
  * Get all API keys
@@ -491,8 +845,8 @@ var ApiKeysClient = class extends BaseClient {
491
845
 
492
846
  // src/client/organizations-client.ts
493
847
  var OrganizationsClient = class extends BaseClient {
494
- constructor(http) {
495
- super(http, "/api/v1");
848
+ constructor(http, cache) {
849
+ super(http, "/api/v1", cache);
496
850
  }
497
851
  /**
498
852
  * Get all organizations
@@ -564,8 +918,8 @@ var OrganizationsClient = class extends BaseClient {
564
918
 
565
919
  // src/client/sites-client.ts
566
920
  var SitesClient = class extends BaseClient {
567
- constructor(http) {
568
- super(http, "/api/v1");
921
+ constructor(http, cache) {
922
+ super(http, "/api/v1", cache);
569
923
  }
570
924
  /**
571
925
  * Get all sites
@@ -661,13 +1015,13 @@ var SitesClient = class extends BaseClient {
661
1015
 
662
1016
  // src/client/products-client.ts
663
1017
  var ProductsClient = class extends BaseClient {
664
- constructor(http) {
665
- super(http, "/api/v1");
1018
+ constructor(http, cache) {
1019
+ super(http, "/api/v1", cache);
666
1020
  }
667
1021
  /**
668
1022
  * Get all products for a site
669
1023
  */
670
- async getProducts(siteName, params) {
1024
+ async getProducts(siteName, params, cachePolicy) {
671
1025
  const normalizeList = (value) => {
672
1026
  if (value === void 0 || value === null) {
673
1027
  return void 0;
@@ -691,30 +1045,61 @@ var ProductsClient = class extends BaseClient {
691
1045
  delete normalizedParams.category_id;
692
1046
  }
693
1047
  }
694
- return this.http.get(
695
- this.buildPath(this.siteScopedEndpoint(siteName, "/products", { includeSitesSegment: false })),
696
- normalizedParams
1048
+ const endpoint = this.siteScopedEndpoint(siteName, "/products", { includeSitesSegment: false });
1049
+ const path = this.buildPath(endpoint);
1050
+ return this.fetchWithCache(
1051
+ endpoint,
1052
+ normalizedParams,
1053
+ this.buildProductTags(siteName, ["products:list"]),
1054
+ cachePolicy,
1055
+ () => this.http.get(path, normalizedParams)
697
1056
  );
698
1057
  }
699
1058
  /**
700
1059
  * Get product by ID
701
1060
  */
702
- async getProductById(id) {
703
- return this.getSingle(`/products/${id}`);
1061
+ async getProductById(id, cachePolicy) {
1062
+ const endpoint = `/products/${id}`;
1063
+ const path = this.buildPath(endpoint);
1064
+ return this.fetchWithCache(
1065
+ endpoint,
1066
+ void 0,
1067
+ this.buildProductTags(void 0, [`products:id:${id}`]),
1068
+ cachePolicy,
1069
+ () => this.http.get(path)
1070
+ );
704
1071
  }
705
1072
  /**
706
1073
  * Get product by SKU
707
1074
  */
708
- async getProductBySku(sku) {
709
- return this.getSingle(`/products/sku/${sku}`);
1075
+ async getProductBySku(sku, cachePolicy) {
1076
+ const endpoint = `/products/sku/${encodeURIComponent(sku)}`;
1077
+ const path = this.buildPath(endpoint);
1078
+ return this.fetchWithCache(
1079
+ endpoint,
1080
+ void 0,
1081
+ this.buildProductTags(void 0, [`products:sku:${sku.toLowerCase()}`]),
1082
+ cachePolicy,
1083
+ () => this.http.get(path)
1084
+ );
710
1085
  }
711
1086
  /**
712
1087
  * Get product by slug and site name
713
1088
  */
714
- async getProductBySlug(siteName, slug) {
715
- return this.http.get(this.buildPath(
716
- this.siteScopedEndpoint(siteName, `/products/slug/${encodeURIComponent(slug)}`, { includeSitesSegment: false })
717
- ));
1089
+ async getProductBySlug(siteName, slug, cachePolicy) {
1090
+ const endpoint = this.siteScopedEndpoint(
1091
+ siteName,
1092
+ `/products/slug/${encodeURIComponent(slug)}`,
1093
+ { includeSitesSegment: false }
1094
+ );
1095
+ const path = this.buildPath(endpoint);
1096
+ return this.fetchWithCache(
1097
+ endpoint,
1098
+ void 0,
1099
+ this.buildProductTags(siteName, [`products:slug:${siteName}:${slug}`]),
1100
+ cachePolicy,
1101
+ () => this.http.get(path)
1102
+ );
718
1103
  }
719
1104
  /**
720
1105
  * Create new product
@@ -803,27 +1188,44 @@ var ProductsClient = class extends BaseClient {
803
1188
  /**
804
1189
  * Get products by category slug
805
1190
  */
806
- async getProductsByCategorySlug(siteName, categorySlug, params) {
1191
+ async getProductsByCategorySlug(siteName, categorySlug, params, cachePolicy) {
807
1192
  const queryParams = params ? {
808
1193
  limit: params.limit,
809
1194
  offset: params.page ? (params.page - 1) * (params.limit || 20) : void 0,
810
1195
  published: params.published,
811
1196
  search: params.search
812
1197
  } : 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);
1198
+ const endpoint = this.siteScopedEndpoint(
1199
+ siteName,
1200
+ `/products/category/${encodeURIComponent(categorySlug)}`,
1201
+ { includeSitesSegment: false }
1202
+ );
1203
+ const path = this.buildPath(endpoint);
1204
+ return this.fetchWithCache(
1205
+ endpoint,
1206
+ queryParams,
1207
+ this.buildProductTags(siteName, [
1208
+ "products:category",
1209
+ `products:category:${siteName}:${categorySlug}`
1210
+ ]),
1211
+ cachePolicy,
1212
+ () => this.http.get(path, queryParams)
1213
+ );
1214
+ }
1215
+ buildProductTags(siteName, extraTags = []) {
1216
+ const tags = /* @__PURE__ */ new Set(["products"]);
1217
+ if (siteName) {
1218
+ tags.add(`products:site:${siteName}`);
1219
+ }
1220
+ extraTags.filter(Boolean).forEach((tag) => tags.add(tag));
1221
+ return Array.from(tags.values());
820
1222
  }
821
1223
  };
822
1224
 
823
1225
  // src/client/categories-client.ts
824
1226
  var CategoriesClient = class extends BaseClient {
825
- constructor(http) {
826
- super(http, "/api/v1");
1227
+ constructor(http, cache) {
1228
+ super(http, "/api/v1", cache);
827
1229
  }
828
1230
  /**
829
1231
  * Get all categories
@@ -846,10 +1248,20 @@ var CategoriesClient = class extends BaseClient {
846
1248
  /**
847
1249
  * Get product category by slug (for products)
848
1250
  */
849
- async getProductCategoryBySlug(siteName, slug) {
850
- return this.http.get(this.buildPath(
851
- this.siteScopedEndpoint(siteName, `/product_category/slug/${encodeURIComponent(slug)}`)
852
- ));
1251
+ async getProductCategoryBySlug(siteName, slug, cachePolicy) {
1252
+ const endpoint = this.siteScopedEndpoint(
1253
+ siteName,
1254
+ `/product_category/slug/${encodeURIComponent(slug)}`,
1255
+ { includeSitesSegment: false }
1256
+ );
1257
+ const path = this.buildPath(endpoint);
1258
+ return this.fetchWithCache(
1259
+ endpoint,
1260
+ void 0,
1261
+ this.buildCategoryTags(siteName, slug),
1262
+ cachePolicy,
1263
+ () => this.http.get(path)
1264
+ );
853
1265
  }
854
1266
  /**
855
1267
  * Create new category
@@ -918,12 +1330,22 @@ var CategoriesClient = class extends BaseClient {
918
1330
  async searchCategories(query, params) {
919
1331
  return this.http.get(`/categories/search`, { q: query, ...params });
920
1332
  }
1333
+ buildCategoryTags(siteName, slug) {
1334
+ const tags = /* @__PURE__ */ new Set(["categories"]);
1335
+ if (siteName) {
1336
+ tags.add(`categories:site:${siteName}`);
1337
+ }
1338
+ if (slug) {
1339
+ tags.add(`categories:product:${siteName}:${slug}`);
1340
+ }
1341
+ return Array.from(tags.values());
1342
+ }
921
1343
  };
922
1344
 
923
1345
  // src/client/webhooks-client.ts
924
1346
  var WebhooksClient = class extends BaseClient {
925
- constructor(http) {
926
- super(http, "/api/v1");
1347
+ constructor(http, cache) {
1348
+ super(http, "/api/v1", cache);
927
1349
  }
928
1350
  /**
929
1351
  * Get all webhooks
@@ -1020,8 +1442,8 @@ var WebhooksClient = class extends BaseClient {
1020
1442
 
1021
1443
  // src/client/checkout-client.ts
1022
1444
  var CheckoutClient = class extends BaseClient {
1023
- constructor(http) {
1024
- super(http, "/api/v1");
1445
+ constructor(http, cache) {
1446
+ super(http, "/api/v1", cache);
1025
1447
  }
1026
1448
  /**
1027
1449
  * Get CSRF token for a specific site
@@ -1047,7 +1469,7 @@ var CheckoutClient = class extends BaseClient {
1047
1469
  token = csrfToken2;
1048
1470
  }
1049
1471
  return this.http.request(this.buildPath(
1050
- this.siteScopedEndpoint(siteName, "/checkout/create-session")
1472
+ this.siteScopedEndpoint(siteName, "/checkout/create-session", { includeSitesSegment: false })
1051
1473
  ), {
1052
1474
  method: "POST",
1053
1475
  body: data,
@@ -1132,8 +1554,8 @@ var CheckoutClient = class extends BaseClient {
1132
1554
 
1133
1555
  // src/client/contact-client.ts
1134
1556
  var ContactClient = class extends BaseClient {
1135
- constructor(http) {
1136
- super(http, "/api/v1");
1557
+ constructor(http, cache) {
1558
+ super(http, "/api/v1", cache);
1137
1559
  }
1138
1560
  /**
1139
1561
  * Build a contact endpoint scoped to a site (without /sites prefix)
@@ -1239,8 +1661,8 @@ var ContactClient = class extends BaseClient {
1239
1661
 
1240
1662
  // src/client/newsletter-client.ts
1241
1663
  var NewsletterClient = class extends BaseClient {
1242
- constructor(http) {
1243
- super(http, "/api/v1");
1664
+ constructor(http, cache) {
1665
+ super(http, "/api/v1", cache);
1244
1666
  }
1245
1667
  /**
1246
1668
  * Build a newsletter endpoint scoped to a site (without /sites prefix)
@@ -1435,6 +1857,7 @@ var NewsletterClient = class extends BaseClient {
1435
1857
  // src/perspect-api-client.ts
1436
1858
  var PerspectApiClient = class {
1437
1859
  http;
1860
+ cache;
1438
1861
  // Service clients
1439
1862
  auth;
1440
1863
  content;
@@ -1452,17 +1875,18 @@ var PerspectApiClient = class {
1452
1875
  throw new Error("baseUrl is required in PerspectApiConfig");
1453
1876
  }
1454
1877
  this.http = new HttpClient(config);
1455
- this.auth = new AuthClient(this.http);
1456
- this.content = new ContentClient(this.http);
1457
- this.apiKeys = new ApiKeysClient(this.http);
1458
- this.organizations = new OrganizationsClient(this.http);
1459
- this.sites = new SitesClient(this.http);
1460
- this.products = new ProductsClient(this.http);
1461
- this.categories = new CategoriesClient(this.http);
1462
- this.webhooks = new WebhooksClient(this.http);
1463
- this.checkout = new CheckoutClient(this.http);
1464
- this.contact = new ContactClient(this.http);
1465
- this.newsletter = new NewsletterClient(this.http);
1878
+ this.cache = new CacheManager(config.cache);
1879
+ this.auth = new AuthClient(this.http, this.cache);
1880
+ this.content = new ContentClient(this.http, this.cache);
1881
+ this.apiKeys = new ApiKeysClient(this.http, this.cache);
1882
+ this.organizations = new OrganizationsClient(this.http, this.cache);
1883
+ this.sites = new SitesClient(this.http, this.cache);
1884
+ this.products = new ProductsClient(this.http, this.cache);
1885
+ this.categories = new CategoriesClient(this.http, this.cache);
1886
+ this.webhooks = new WebhooksClient(this.http, this.cache);
1887
+ this.checkout = new CheckoutClient(this.http, this.cache);
1888
+ this.contact = new ContactClient(this.http, this.cache);
1889
+ this.newsletter = new NewsletterClient(this.http, this.cache);
1466
1890
  }
1467
1891
  /**
1468
1892
  * Update authentication token
@@ -2042,13 +2466,16 @@ export {
2042
2466
  ApiKeysClient,
2043
2467
  AuthClient,
2044
2468
  BaseClient,
2469
+ CacheManager,
2045
2470
  CategoriesClient,
2046
2471
  CheckoutClient,
2047
2472
  ContactClient,
2048
2473
  ContentClient,
2049
2474
  DEFAULT_IMAGE_SIZES,
2050
2475
  HttpClient,
2476
+ InMemoryCacheAdapter,
2051
2477
  NewsletterClient,
2478
+ NoopCacheAdapter,
2052
2479
  OrganizationsClient,
2053
2480
  PerspectApiClient,
2054
2481
  ProductsClient,