perspectapi-ts-sdk 2.8.2 → 2.9.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/README.md CHANGED
@@ -13,6 +13,7 @@ A comprehensive TypeScript SDK for PerspectAPI, designed to work seamlessly with
13
13
  - 📊 **Comprehensive Coverage** - All PerspectAPI endpoints supported
14
14
  - 🧩 **High-Level Loaders** - Drop-in helpers for products, content, and checkout flows with fallbacks
15
15
  - 📧 **Newsletter Management** - Complete newsletter subscription system with double opt-in, preferences, and lists
16
+ - 👥 **Site Users** - OTP-based customer accounts with metadata, profiles, orders, and subscriptions
16
17
 
17
18
  ## Installation
18
19
 
@@ -734,6 +735,100 @@ console.log('Imported:', importResult.data.imported);
734
735
  console.log('Failed:', importResult.data.failed);
735
736
  ```
736
737
 
738
+ > 📚 **For complete newsletter documentation**, see [docs/newsletter.md](docs/newsletter.md)
739
+
740
+ ### Site Users (Customer Accounts)
741
+
742
+ Site users are per-site customer accounts with OTP-based authentication, separate from admin users.
743
+
744
+ ```typescript
745
+ const siteName = 'your-site-name';
746
+
747
+ // Request OTP for login/signup (with optional metadata)
748
+ await client.siteUsers.requestOtp(
749
+ siteName,
750
+ {
751
+ email: 'user@example.com',
752
+ waitlist: true, // Optional: mark as waitlist signup
753
+ metadata: { // Optional: set metadata at signup time
754
+ signupSource: 'landing-page',
755
+ referralCode: 'FRIEND123',
756
+ interests: ['product-updates']
757
+ }
758
+ },
759
+ csrfToken
760
+ );
761
+
762
+ // Verify OTP and get JWT token
763
+ const response = await client.siteUsers.verifyOtp(
764
+ siteName,
765
+ { email: 'user@example.com', code: '123456' },
766
+ csrfToken
767
+ );
768
+
769
+ const { token, user } = response.data;
770
+ client.setAuth(token); // Set auth for subsequent requests
771
+
772
+ // Get current user profile
773
+ const { data } = await client.siteUsers.getMe(siteName);
774
+ console.log(data.user); // Basic user fields + metadata
775
+ console.log(data.profile); // Key-value profile data
776
+
777
+ // Update user profile and metadata
778
+ await client.siteUsers.updateMe(
779
+ siteName,
780
+ {
781
+ first_name: 'John',
782
+ last_name: 'Doe',
783
+ metadata: {
784
+ preferences: { theme: 'dark' },
785
+ tags: ['premium'],
786
+ customField: 'value'
787
+ }
788
+ },
789
+ csrfToken
790
+ );
791
+
792
+ // Set profile key-values (phone, addresses, etc.)
793
+ await client.siteUsers.setProfileValue(
794
+ siteName,
795
+ 'phone',
796
+ '+1-555-0123',
797
+ csrfToken
798
+ );
799
+
800
+ await client.siteUsers.setProfileValue(
801
+ siteName,
802
+ 'address_shipping',
803
+ JSON.stringify({
804
+ line1: '123 Main St',
805
+ city: 'San Francisco',
806
+ state: 'CA',
807
+ postal_code: '94102',
808
+ country: 'US'
809
+ }),
810
+ csrfToken
811
+ );
812
+
813
+ // Get order history
814
+ const orders = await client.siteUsers.getOrders(siteName, {
815
+ limit: 50,
816
+ offset: 0
817
+ });
818
+
819
+ // Get subscriptions
820
+ const subscriptions = await client.siteUsers.getSubscriptions(siteName);
821
+
822
+ // Cancel subscription
823
+ await client.siteUsers.cancelSubscription(siteName, 'sub_123', csrfToken);
824
+
825
+ // Logout
826
+ await client.siteUsers.logout(siteName);
827
+ client.setAuth(null);
828
+ ```
829
+
830
+ > 📚 **For complete site users documentation** including waitlist management, metadata patterns, cross-domain authentication, and complete examples, see [docs/site-users.md](docs/site-users.md)
831
+
737
832
  ## Configuration Options
738
833
 
739
834
  ```typescript
package/dist/index.d.mts CHANGED
@@ -627,6 +627,7 @@ interface SiteUserOrder {
627
627
  interface RequestOtpRequest {
628
628
  email: string;
629
629
  waitlist?: boolean;
630
+ metadata?: Record<string, any>;
630
631
  }
631
632
  interface VerifyOtpRequest {
632
633
  email: string;
@@ -1410,95 +1411,66 @@ declare class ProductsClient extends BaseClient {
1410
1411
  declare class CategoriesClient extends BaseClient {
1411
1412
  constructor(http: any, cache?: CacheManager);
1412
1413
  /**
1413
- * Get all categories
1414
- */
1415
- getCategories(params?: {
1416
- page?: number;
1417
- limit?: number;
1418
- parentId?: number;
1419
- organizationId?: number;
1420
- }): Promise<PaginatedResponse<Category>>;
1421
- /**
1422
- * Get category by ID
1414
+ * Get all categories for a site
1423
1415
  */
1424
- getCategoryById(id: number): Promise<ApiResponse<Category>>;
1425
- /**
1426
- * Get category by slug
1427
- */
1428
- getCategoryBySlug(slug: string): Promise<ApiResponse<Category>>;
1429
- /**
1430
- * Get product category by slug (for products)
1431
- */
1432
- getProductCategoryBySlug(siteName: string, slug: string, cachePolicy?: CachePolicy): Promise<ApiResponse<Category>>;
1416
+ getCategories(siteName: string, params?: {
1417
+ category_type?: 'post' | 'product';
1418
+ parent_id?: string;
1419
+ include_subcategories?: 'true' | 'false';
1420
+ }, cachePolicy?: CachePolicy): Promise<ApiResponse<{
1421
+ categories: Category[];
1422
+ }>>;
1433
1423
  /**
1434
- * Create new category
1424
+ * Get category by ID (validates it belongs to the site)
1435
1425
  */
1436
- createCategory(data: CreateCategoryRequest): Promise<ApiResponse<Category>>;
1426
+ getCategoryById(siteName: string, id: number, cachePolicy?: CachePolicy): Promise<ApiResponse<{
1427
+ category: Category;
1428
+ }>>;
1437
1429
  /**
1438
- * Update category
1430
+ * Get product categories for a site
1439
1431
  */
1440
- updateCategory(id: number, data: Partial<CreateCategoryRequest>): Promise<ApiResponse<Category>>;
1432
+ getProductCategories(siteName: string, params?: {
1433
+ parent_id?: string;
1434
+ include_subcategories?: 'true' | 'false';
1435
+ }, cachePolicy?: CachePolicy): Promise<ApiResponse<{
1436
+ categories: Category[];
1437
+ }>>;
1441
1438
  /**
1442
- * Delete category
1439
+ * Create new category for a site
1443
1440
  */
1444
- deleteCategory(id: number): Promise<ApiResponse<{
1441
+ createCategory(siteName: string, data: CreateCategoryRequest, csrfToken?: string): Promise<ApiResponse<{
1445
1442
  message: string;
1443
+ category_id: number;
1446
1444
  }>>;
1447
1445
  /**
1448
- * Get category tree (hierarchical structure)
1449
- */
1450
- getCategoryTree(rootId?: number): Promise<ApiResponse<Array<Category & {
1451
- children: Category[];
1452
- }>>>;
1453
- /**
1454
- * Get category children
1446
+ * Create new product category for a site
1455
1447
  */
1456
- getCategoryChildren(id: number): Promise<ApiResponse<Category[]>>;
1457
- /**
1458
- * Get category parent
1459
- */
1460
- getCategoryParent(id: number): Promise<ApiResponse<Category | null>>;
1461
- /**
1462
- * Move category to new parent
1463
- */
1464
- moveCategoryToParent(id: number, parentId: number | null): Promise<ApiResponse<Category>>;
1465
- /**
1466
- * Get category breadcrumb path
1467
- */
1468
- getCategoryBreadcrumb(id: number): Promise<ApiResponse<Array<{
1469
- id: number;
1470
- name: string;
1471
- slug: string;
1472
- }>>>;
1448
+ createProductCategory(siteName: string, data: Omit<CreateCategoryRequest, 'category_type'>, csrfToken?: string): Promise<ApiResponse<{
1449
+ message: string;
1450
+ category_id: number;
1451
+ }>>;
1473
1452
  /**
1474
- * Get category content/products
1453
+ * Update category (validates it belongs to the site)
1475
1454
  */
1476
- getCategoryContent(id: number, params?: {
1477
- page?: number;
1478
- limit?: number;
1479
- type?: 'content' | 'products' | 'all';
1480
- }): Promise<ApiResponse<{
1481
- content: any[];
1482
- products: any[];
1483
- total: number;
1455
+ updateCategory(siteName: string, id: number, data: Partial<CreateCategoryRequest>, csrfToken?: string): Promise<ApiResponse<{
1456
+ message: string;
1484
1457
  }>>;
1485
1458
  /**
1486
- * Bulk update category order
1459
+ * Delete category (validates it belongs to the site)
1487
1460
  */
1488
- updateCategoryOrder(updates: Array<{
1489
- id: number;
1490
- order: number;
1491
- parentId?: number;
1492
- }>): Promise<ApiResponse<{
1461
+ deleteCategory(siteName: string, id: number, csrfToken?: string): Promise<ApiResponse<{
1493
1462
  message: string;
1494
1463
  }>>;
1495
1464
  /**
1496
- * Search categories
1465
+ * Associate pages or products with categories
1497
1466
  */
1498
- searchCategories(query: string, params?: {
1499
- limit?: number;
1500
- organizationId?: number;
1501
- }): Promise<ApiResponse<Category[]>>;
1467
+ associateCategories(siteName: string, data: {
1468
+ entity_id: number;
1469
+ entity_type: 'page' | 'product';
1470
+ category_ids: number[];
1471
+ }, csrfToken?: string): Promise<ApiResponse<{
1472
+ message: string;
1473
+ }>>;
1502
1474
  private buildCategoryTags;
1503
1475
  }
1504
1476
 
package/dist/index.d.ts CHANGED
@@ -627,6 +627,7 @@ interface SiteUserOrder {
627
627
  interface RequestOtpRequest {
628
628
  email: string;
629
629
  waitlist?: boolean;
630
+ metadata?: Record<string, any>;
630
631
  }
631
632
  interface VerifyOtpRequest {
632
633
  email: string;
@@ -1410,95 +1411,66 @@ declare class ProductsClient extends BaseClient {
1410
1411
  declare class CategoriesClient extends BaseClient {
1411
1412
  constructor(http: any, cache?: CacheManager);
1412
1413
  /**
1413
- * Get all categories
1414
- */
1415
- getCategories(params?: {
1416
- page?: number;
1417
- limit?: number;
1418
- parentId?: number;
1419
- organizationId?: number;
1420
- }): Promise<PaginatedResponse<Category>>;
1421
- /**
1422
- * Get category by ID
1414
+ * Get all categories for a site
1423
1415
  */
1424
- getCategoryById(id: number): Promise<ApiResponse<Category>>;
1425
- /**
1426
- * Get category by slug
1427
- */
1428
- getCategoryBySlug(slug: string): Promise<ApiResponse<Category>>;
1429
- /**
1430
- * Get product category by slug (for products)
1431
- */
1432
- getProductCategoryBySlug(siteName: string, slug: string, cachePolicy?: CachePolicy): Promise<ApiResponse<Category>>;
1416
+ getCategories(siteName: string, params?: {
1417
+ category_type?: 'post' | 'product';
1418
+ parent_id?: string;
1419
+ include_subcategories?: 'true' | 'false';
1420
+ }, cachePolicy?: CachePolicy): Promise<ApiResponse<{
1421
+ categories: Category[];
1422
+ }>>;
1433
1423
  /**
1434
- * Create new category
1424
+ * Get category by ID (validates it belongs to the site)
1435
1425
  */
1436
- createCategory(data: CreateCategoryRequest): Promise<ApiResponse<Category>>;
1426
+ getCategoryById(siteName: string, id: number, cachePolicy?: CachePolicy): Promise<ApiResponse<{
1427
+ category: Category;
1428
+ }>>;
1437
1429
  /**
1438
- * Update category
1430
+ * Get product categories for a site
1439
1431
  */
1440
- updateCategory(id: number, data: Partial<CreateCategoryRequest>): Promise<ApiResponse<Category>>;
1432
+ getProductCategories(siteName: string, params?: {
1433
+ parent_id?: string;
1434
+ include_subcategories?: 'true' | 'false';
1435
+ }, cachePolicy?: CachePolicy): Promise<ApiResponse<{
1436
+ categories: Category[];
1437
+ }>>;
1441
1438
  /**
1442
- * Delete category
1439
+ * Create new category for a site
1443
1440
  */
1444
- deleteCategory(id: number): Promise<ApiResponse<{
1441
+ createCategory(siteName: string, data: CreateCategoryRequest, csrfToken?: string): Promise<ApiResponse<{
1445
1442
  message: string;
1443
+ category_id: number;
1446
1444
  }>>;
1447
1445
  /**
1448
- * Get category tree (hierarchical structure)
1449
- */
1450
- getCategoryTree(rootId?: number): Promise<ApiResponse<Array<Category & {
1451
- children: Category[];
1452
- }>>>;
1453
- /**
1454
- * Get category children
1446
+ * Create new product category for a site
1455
1447
  */
1456
- getCategoryChildren(id: number): Promise<ApiResponse<Category[]>>;
1457
- /**
1458
- * Get category parent
1459
- */
1460
- getCategoryParent(id: number): Promise<ApiResponse<Category | null>>;
1461
- /**
1462
- * Move category to new parent
1463
- */
1464
- moveCategoryToParent(id: number, parentId: number | null): Promise<ApiResponse<Category>>;
1465
- /**
1466
- * Get category breadcrumb path
1467
- */
1468
- getCategoryBreadcrumb(id: number): Promise<ApiResponse<Array<{
1469
- id: number;
1470
- name: string;
1471
- slug: string;
1472
- }>>>;
1448
+ createProductCategory(siteName: string, data: Omit<CreateCategoryRequest, 'category_type'>, csrfToken?: string): Promise<ApiResponse<{
1449
+ message: string;
1450
+ category_id: number;
1451
+ }>>;
1473
1452
  /**
1474
- * Get category content/products
1453
+ * Update category (validates it belongs to the site)
1475
1454
  */
1476
- getCategoryContent(id: number, params?: {
1477
- page?: number;
1478
- limit?: number;
1479
- type?: 'content' | 'products' | 'all';
1480
- }): Promise<ApiResponse<{
1481
- content: any[];
1482
- products: any[];
1483
- total: number;
1455
+ updateCategory(siteName: string, id: number, data: Partial<CreateCategoryRequest>, csrfToken?: string): Promise<ApiResponse<{
1456
+ message: string;
1484
1457
  }>>;
1485
1458
  /**
1486
- * Bulk update category order
1459
+ * Delete category (validates it belongs to the site)
1487
1460
  */
1488
- updateCategoryOrder(updates: Array<{
1489
- id: number;
1490
- order: number;
1491
- parentId?: number;
1492
- }>): Promise<ApiResponse<{
1461
+ deleteCategory(siteName: string, id: number, csrfToken?: string): Promise<ApiResponse<{
1493
1462
  message: string;
1494
1463
  }>>;
1495
1464
  /**
1496
- * Search categories
1465
+ * Associate pages or products with categories
1497
1466
  */
1498
- searchCategories(query: string, params?: {
1499
- limit?: number;
1500
- organizationId?: number;
1501
- }): Promise<ApiResponse<Category[]>>;
1467
+ associateCategories(siteName: string, data: {
1468
+ entity_id: number;
1469
+ entity_type: 'page' | 'product';
1470
+ category_ids: number[];
1471
+ }, csrfToken?: string): Promise<ApiResponse<{
1472
+ message: string;
1473
+ }>>;
1502
1474
  private buildCategoryTags;
1503
1475
  }
1504
1476
 
package/dist/index.js CHANGED
@@ -1589,115 +1589,114 @@ var CategoriesClient = class extends BaseClient {
1589
1589
  super(http, "/api/v1", cache);
1590
1590
  }
1591
1591
  /**
1592
- * Get all categories
1592
+ * Get all categories for a site
1593
1593
  */
1594
- async getCategories(params) {
1595
- return this.getPaginated("/categories", params);
1596
- }
1597
- /**
1598
- * Get category by ID
1599
- */
1600
- async getCategoryById(id) {
1601
- return this.getSingle(`/categories/${id}`);
1602
- }
1603
- /**
1604
- * Get category by slug
1605
- */
1606
- async getCategoryBySlug(slug) {
1607
- return this.getSingle(`/categories/slug/${slug}`);
1594
+ async getCategories(siteName, params, cachePolicy) {
1595
+ const endpoint = this.siteScopedEndpoint(siteName, "/categories", { includeSitesSegment: false });
1596
+ const path = this.buildPath(endpoint);
1597
+ return this.fetchWithCache(
1598
+ endpoint,
1599
+ params,
1600
+ this.buildCategoryTags(siteName),
1601
+ cachePolicy,
1602
+ () => this.http.get(path, params)
1603
+ );
1608
1604
  }
1609
1605
  /**
1610
- * Get product category by slug (for products)
1606
+ * Get category by ID (validates it belongs to the site)
1611
1607
  */
1612
- async getProductCategoryBySlug(siteName, slug, cachePolicy) {
1613
- const endpoint = this.siteScopedEndpoint(
1614
- siteName,
1615
- `/product_category/slug/${encodeURIComponent(slug)}`,
1616
- { includeSitesSegment: false }
1617
- );
1608
+ async getCategoryById(siteName, id, cachePolicy) {
1609
+ const endpoint = this.siteScopedEndpoint(siteName, `/categories/${id}`, { includeSitesSegment: false });
1618
1610
  const path = this.buildPath(endpoint);
1619
1611
  return this.fetchWithCache(
1620
1612
  endpoint,
1621
1613
  void 0,
1622
- this.buildCategoryTags(siteName, slug),
1614
+ this.buildCategoryTags(siteName, `categories:id:${id}`),
1623
1615
  cachePolicy,
1624
1616
  () => this.http.get(path)
1625
1617
  );
1626
1618
  }
1627
1619
  /**
1628
- * Create new category
1620
+ * Get product categories for a site
1629
1621
  */
1630
- async createCategory(data) {
1631
- return this.create("/categories", data);
1632
- }
1633
- /**
1634
- * Update category
1635
- */
1636
- async updateCategory(id, data) {
1637
- return this.update(`/categories/${id}`, data);
1638
- }
1639
- /**
1640
- * Delete category
1641
- */
1642
- async deleteCategory(id) {
1643
- return this.delete(`/categories/${id}`);
1644
- }
1645
- /**
1646
- * Get category tree (hierarchical structure)
1647
- */
1648
- async getCategoryTree(rootId) {
1649
- const endpoint = rootId ? `/categories/tree/${rootId}` : "/categories/tree";
1650
- return this.getSingle(endpoint);
1651
- }
1652
- /**
1653
- * Get category children
1654
- */
1655
- async getCategoryChildren(id) {
1656
- return this.getSingle(`/categories/${id}/children`);
1657
- }
1658
- /**
1659
- * Get category parent
1660
- */
1661
- async getCategoryParent(id) {
1662
- return this.getSingle(`/categories/${id}/parent`);
1622
+ async getProductCategories(siteName, params, cachePolicy) {
1623
+ const endpoint = this.siteScopedEndpoint(siteName, "/categories/product", { includeSitesSegment: false });
1624
+ const path = this.buildPath(endpoint);
1625
+ return this.fetchWithCache(
1626
+ endpoint,
1627
+ params,
1628
+ this.buildCategoryTags(siteName, "categories:product"),
1629
+ cachePolicy,
1630
+ () => this.http.get(path, params)
1631
+ );
1663
1632
  }
1664
1633
  /**
1665
- * Move category to new parent
1634
+ * Create new category for a site
1666
1635
  */
1667
- async moveCategoryToParent(id, parentId) {
1668
- return this.patch(`/categories/${id}`, { parentId });
1636
+ async createCategory(siteName, data, csrfToken) {
1637
+ const endpoint = this.siteScopedEndpoint(siteName, "/categories", { includeSitesSegment: false });
1638
+ const path = this.buildPath(endpoint);
1639
+ const result = await this.http.post(path, data, { csrfToken });
1640
+ if (this.cache) {
1641
+ await this.cache.invalidate({ tags: this.buildCategoryTags(siteName) });
1642
+ }
1643
+ return result;
1669
1644
  }
1670
1645
  /**
1671
- * Get category breadcrumb path
1646
+ * Create new product category for a site
1672
1647
  */
1673
- async getCategoryBreadcrumb(id) {
1674
- return this.getSingle(`/categories/${id}/breadcrumb`);
1648
+ async createProductCategory(siteName, data, csrfToken) {
1649
+ const endpoint = this.siteScopedEndpoint(siteName, "/categories/product", { includeSitesSegment: false });
1650
+ const path = this.buildPath(endpoint);
1651
+ const result = await this.http.post(path, data, { csrfToken });
1652
+ if (this.cache) {
1653
+ await this.cache.invalidate({ tags: this.buildCategoryTags(siteName, "categories:product") });
1654
+ }
1655
+ return result;
1675
1656
  }
1676
1657
  /**
1677
- * Get category content/products
1658
+ * Update category (validates it belongs to the site)
1678
1659
  */
1679
- async getCategoryContent(id, params) {
1680
- return this.http.get(`/categories/${id}/content`, params);
1660
+ async updateCategory(siteName, id, data, csrfToken) {
1661
+ const endpoint = this.siteScopedEndpoint(siteName, `/categories/${id}`, { includeSitesSegment: false });
1662
+ const path = this.buildPath(endpoint);
1663
+ const result = await this.http.put(path, data, { csrfToken });
1664
+ if (this.cache) {
1665
+ await this.cache.invalidate({ tags: this.buildCategoryTags(siteName, `categories:id:${id}`) });
1666
+ }
1667
+ return result;
1681
1668
  }
1682
1669
  /**
1683
- * Bulk update category order
1670
+ * Delete category (validates it belongs to the site)
1684
1671
  */
1685
- async updateCategoryOrder(updates) {
1686
- return this.create("/categories/reorder", { updates });
1672
+ async deleteCategory(siteName, id, csrfToken) {
1673
+ const endpoint = this.siteScopedEndpoint(siteName, `/categories/${id}`, { includeSitesSegment: false });
1674
+ const path = this.buildPath(endpoint);
1675
+ const result = await this.http.delete(path, { csrfToken });
1676
+ if (this.cache) {
1677
+ await this.cache.invalidate({ tags: this.buildCategoryTags(siteName) });
1678
+ }
1679
+ return result;
1687
1680
  }
1688
1681
  /**
1689
- * Search categories
1682
+ * Associate pages or products with categories
1690
1683
  */
1691
- async searchCategories(query, params) {
1692
- return this.http.get(`/categories/search`, { q: query, ...params });
1684
+ async associateCategories(siteName, data, csrfToken) {
1685
+ const endpoint = this.siteScopedEndpoint(siteName, "/categories/associate", { includeSitesSegment: false });
1686
+ const path = this.buildPath(endpoint);
1687
+ const result = await this.http.post(path, data, { csrfToken });
1688
+ if (this.cache) {
1689
+ await this.cache.invalidate({ tags: this.buildCategoryTags(siteName) });
1690
+ }
1691
+ return result;
1693
1692
  }
1694
- buildCategoryTags(siteName, slug) {
1693
+ buildCategoryTags(siteName, extraTag) {
1695
1694
  const tags = /* @__PURE__ */ new Set(["categories"]);
1696
1695
  if (siteName) {
1697
1696
  tags.add(`categories:site:${siteName}`);
1698
1697
  }
1699
- if (slug) {
1700
- tags.add(`categories:product:${siteName}:${slug}`);
1698
+ if (extraTag) {
1699
+ tags.add(extraTag);
1701
1700
  }
1702
1701
  return Array.from(tags.values());
1703
1702
  }
package/dist/index.mjs CHANGED
@@ -1527,115 +1527,114 @@ var CategoriesClient = class extends BaseClient {
1527
1527
  super(http, "/api/v1", cache);
1528
1528
  }
1529
1529
  /**
1530
- * Get all categories
1530
+ * Get all categories for a site
1531
1531
  */
1532
- async getCategories(params) {
1533
- return this.getPaginated("/categories", params);
1534
- }
1535
- /**
1536
- * Get category by ID
1537
- */
1538
- async getCategoryById(id) {
1539
- return this.getSingle(`/categories/${id}`);
1540
- }
1541
- /**
1542
- * Get category by slug
1543
- */
1544
- async getCategoryBySlug(slug) {
1545
- return this.getSingle(`/categories/slug/${slug}`);
1532
+ async getCategories(siteName, params, cachePolicy) {
1533
+ const endpoint = this.siteScopedEndpoint(siteName, "/categories", { includeSitesSegment: false });
1534
+ const path = this.buildPath(endpoint);
1535
+ return this.fetchWithCache(
1536
+ endpoint,
1537
+ params,
1538
+ this.buildCategoryTags(siteName),
1539
+ cachePolicy,
1540
+ () => this.http.get(path, params)
1541
+ );
1546
1542
  }
1547
1543
  /**
1548
- * Get product category by slug (for products)
1544
+ * Get category by ID (validates it belongs to the site)
1549
1545
  */
1550
- async getProductCategoryBySlug(siteName, slug, cachePolicy) {
1551
- const endpoint = this.siteScopedEndpoint(
1552
- siteName,
1553
- `/product_category/slug/${encodeURIComponent(slug)}`,
1554
- { includeSitesSegment: false }
1555
- );
1546
+ async getCategoryById(siteName, id, cachePolicy) {
1547
+ const endpoint = this.siteScopedEndpoint(siteName, `/categories/${id}`, { includeSitesSegment: false });
1556
1548
  const path = this.buildPath(endpoint);
1557
1549
  return this.fetchWithCache(
1558
1550
  endpoint,
1559
1551
  void 0,
1560
- this.buildCategoryTags(siteName, slug),
1552
+ this.buildCategoryTags(siteName, `categories:id:${id}`),
1561
1553
  cachePolicy,
1562
1554
  () => this.http.get(path)
1563
1555
  );
1564
1556
  }
1565
1557
  /**
1566
- * Create new category
1558
+ * Get product categories for a site
1567
1559
  */
1568
- async createCategory(data) {
1569
- return this.create("/categories", data);
1570
- }
1571
- /**
1572
- * Update category
1573
- */
1574
- async updateCategory(id, data) {
1575
- return this.update(`/categories/${id}`, data);
1576
- }
1577
- /**
1578
- * Delete category
1579
- */
1580
- async deleteCategory(id) {
1581
- return this.delete(`/categories/${id}`);
1582
- }
1583
- /**
1584
- * Get category tree (hierarchical structure)
1585
- */
1586
- async getCategoryTree(rootId) {
1587
- const endpoint = rootId ? `/categories/tree/${rootId}` : "/categories/tree";
1588
- return this.getSingle(endpoint);
1589
- }
1590
- /**
1591
- * Get category children
1592
- */
1593
- async getCategoryChildren(id) {
1594
- return this.getSingle(`/categories/${id}/children`);
1595
- }
1596
- /**
1597
- * Get category parent
1598
- */
1599
- async getCategoryParent(id) {
1600
- return this.getSingle(`/categories/${id}/parent`);
1560
+ async getProductCategories(siteName, params, cachePolicy) {
1561
+ const endpoint = this.siteScopedEndpoint(siteName, "/categories/product", { includeSitesSegment: false });
1562
+ const path = this.buildPath(endpoint);
1563
+ return this.fetchWithCache(
1564
+ endpoint,
1565
+ params,
1566
+ this.buildCategoryTags(siteName, "categories:product"),
1567
+ cachePolicy,
1568
+ () => this.http.get(path, params)
1569
+ );
1601
1570
  }
1602
1571
  /**
1603
- * Move category to new parent
1572
+ * Create new category for a site
1604
1573
  */
1605
- async moveCategoryToParent(id, parentId) {
1606
- return this.patch(`/categories/${id}`, { parentId });
1574
+ async createCategory(siteName, data, csrfToken) {
1575
+ const endpoint = this.siteScopedEndpoint(siteName, "/categories", { includeSitesSegment: false });
1576
+ const path = this.buildPath(endpoint);
1577
+ const result = await this.http.post(path, data, { csrfToken });
1578
+ if (this.cache) {
1579
+ await this.cache.invalidate({ tags: this.buildCategoryTags(siteName) });
1580
+ }
1581
+ return result;
1607
1582
  }
1608
1583
  /**
1609
- * Get category breadcrumb path
1584
+ * Create new product category for a site
1610
1585
  */
1611
- async getCategoryBreadcrumb(id) {
1612
- return this.getSingle(`/categories/${id}/breadcrumb`);
1586
+ async createProductCategory(siteName, data, csrfToken) {
1587
+ const endpoint = this.siteScopedEndpoint(siteName, "/categories/product", { includeSitesSegment: false });
1588
+ const path = this.buildPath(endpoint);
1589
+ const result = await this.http.post(path, data, { csrfToken });
1590
+ if (this.cache) {
1591
+ await this.cache.invalidate({ tags: this.buildCategoryTags(siteName, "categories:product") });
1592
+ }
1593
+ return result;
1613
1594
  }
1614
1595
  /**
1615
- * Get category content/products
1596
+ * Update category (validates it belongs to the site)
1616
1597
  */
1617
- async getCategoryContent(id, params) {
1618
- return this.http.get(`/categories/${id}/content`, params);
1598
+ async updateCategory(siteName, id, data, csrfToken) {
1599
+ const endpoint = this.siteScopedEndpoint(siteName, `/categories/${id}`, { includeSitesSegment: false });
1600
+ const path = this.buildPath(endpoint);
1601
+ const result = await this.http.put(path, data, { csrfToken });
1602
+ if (this.cache) {
1603
+ await this.cache.invalidate({ tags: this.buildCategoryTags(siteName, `categories:id:${id}`) });
1604
+ }
1605
+ return result;
1619
1606
  }
1620
1607
  /**
1621
- * Bulk update category order
1608
+ * Delete category (validates it belongs to the site)
1622
1609
  */
1623
- async updateCategoryOrder(updates) {
1624
- return this.create("/categories/reorder", { updates });
1610
+ async deleteCategory(siteName, id, csrfToken) {
1611
+ const endpoint = this.siteScopedEndpoint(siteName, `/categories/${id}`, { includeSitesSegment: false });
1612
+ const path = this.buildPath(endpoint);
1613
+ const result = await this.http.delete(path, { csrfToken });
1614
+ if (this.cache) {
1615
+ await this.cache.invalidate({ tags: this.buildCategoryTags(siteName) });
1616
+ }
1617
+ return result;
1625
1618
  }
1626
1619
  /**
1627
- * Search categories
1620
+ * Associate pages or products with categories
1628
1621
  */
1629
- async searchCategories(query, params) {
1630
- return this.http.get(`/categories/search`, { q: query, ...params });
1622
+ async associateCategories(siteName, data, csrfToken) {
1623
+ const endpoint = this.siteScopedEndpoint(siteName, "/categories/associate", { includeSitesSegment: false });
1624
+ const path = this.buildPath(endpoint);
1625
+ const result = await this.http.post(path, data, { csrfToken });
1626
+ if (this.cache) {
1627
+ await this.cache.invalidate({ tags: this.buildCategoryTags(siteName) });
1628
+ }
1629
+ return result;
1631
1630
  }
1632
- buildCategoryTags(siteName, slug) {
1631
+ buildCategoryTags(siteName, extraTag) {
1633
1632
  const tags = /* @__PURE__ */ new Set(["categories"]);
1634
1633
  if (siteName) {
1635
1634
  tags.add(`categories:site:${siteName}`);
1636
1635
  }
1637
- if (slug) {
1638
- tags.add(`categories:product:${siteName}:${slug}`);
1636
+ if (extraTag) {
1637
+ tags.add(extraTag);
1639
1638
  }
1640
1639
  return Array.from(tags.values());
1641
1640
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perspectapi-ts-sdk",
3
- "version": "2.8.2",
3
+ "version": "2.9.0",
4
4
  "description": "TypeScript SDK for PerspectAPI - Cloudflare Workers compatible",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -8,7 +8,6 @@ import type { CachePolicy } from '../cache/types';
8
8
  import type {
9
9
  Category,
10
10
  CreateCategoryRequest,
11
- PaginatedResponse,
12
11
  ApiResponse,
13
12
  } from '../types';
14
13
 
@@ -18,161 +17,181 @@ export class CategoriesClient extends BaseClient {
18
17
  }
19
18
 
20
19
  /**
21
- * Get all categories
20
+ * Get all categories for a site
22
21
  */
23
- async getCategories(params?: {
24
- page?: number;
25
- limit?: number;
26
- parentId?: number;
27
- organizationId?: number;
28
- }): Promise<PaginatedResponse<Category>> {
29
- return this.getPaginated<Category>('/categories', params);
30
- }
22
+ async getCategories(
23
+ siteName: string,
24
+ params?: {
25
+ category_type?: 'post' | 'product';
26
+ parent_id?: string;
27
+ include_subcategories?: 'true' | 'false';
28
+ },
29
+ cachePolicy?: CachePolicy
30
+ ): Promise<ApiResponse<{ categories: Category[] }>> {
31
+ const endpoint = this.siteScopedEndpoint(siteName, '/categories', { includeSitesSegment: false });
32
+ const path = this.buildPath(endpoint);
31
33
 
32
- /**
33
- * Get category by ID
34
- */
35
- async getCategoryById(id: number): Promise<ApiResponse<Category>> {
36
- return this.getSingle<Category>(`/categories/${id}`);
34
+ return this.fetchWithCache<ApiResponse<{ categories: Category[] }>>(
35
+ endpoint,
36
+ params,
37
+ this.buildCategoryTags(siteName),
38
+ cachePolicy,
39
+ () => this.http.get<{ categories: Category[] }>(path, params)
40
+ );
37
41
  }
38
42
 
39
43
  /**
40
- * Get category by slug
44
+ * Get category by ID (validates it belongs to the site)
41
45
  */
42
- async getCategoryBySlug(slug: string): Promise<ApiResponse<Category>> {
43
- return this.getSingle<Category>(`/categories/slug/${slug}`);
46
+ async getCategoryById(siteName: string, id: number, cachePolicy?: CachePolicy): Promise<ApiResponse<{ category: Category }>> {
47
+ const endpoint = this.siteScopedEndpoint(siteName, `/categories/${id}`, { includeSitesSegment: false });
48
+ const path = this.buildPath(endpoint);
49
+
50
+ return this.fetchWithCache<ApiResponse<{ category: Category }>>(
51
+ endpoint,
52
+ undefined,
53
+ this.buildCategoryTags(siteName, `categories:id:${id}`),
54
+ cachePolicy,
55
+ () => this.http.get<{ category: Category }>(path)
56
+ );
44
57
  }
45
58
 
46
59
  /**
47
- * Get product category by slug (for products)
60
+ * Get product categories for a site
48
61
  */
49
- async getProductCategoryBySlug(
62
+ async getProductCategories(
50
63
  siteName: string,
51
- slug: string,
64
+ params?: {
65
+ parent_id?: string;
66
+ include_subcategories?: 'true' | 'false';
67
+ },
52
68
  cachePolicy?: CachePolicy
53
- ): Promise<ApiResponse<Category>> {
54
- const endpoint = this.siteScopedEndpoint(
55
- siteName,
56
- `/product_category/slug/${encodeURIComponent(slug)}`,
57
- { includeSitesSegment: false }
58
- );
69
+ ): Promise<ApiResponse<{ categories: Category[] }>> {
70
+ const endpoint = this.siteScopedEndpoint(siteName, '/categories/product', { includeSitesSegment: false });
59
71
  const path = this.buildPath(endpoint);
60
72
 
61
- return this.fetchWithCache<ApiResponse<Category>>(
73
+ return this.fetchWithCache<ApiResponse<{ categories: Category[] }>>(
62
74
  endpoint,
63
- undefined,
64
- this.buildCategoryTags(siteName, slug),
75
+ params,
76
+ this.buildCategoryTags(siteName, 'categories:product'),
65
77
  cachePolicy,
66
- () => this.http.get<Category>(path)
78
+ () => this.http.get<{ categories: Category[] }>(path, params)
67
79
  );
68
80
  }
69
81
 
70
82
  /**
71
- * Create new category
83
+ * Create new category for a site
72
84
  */
73
- async createCategory(data: CreateCategoryRequest): Promise<ApiResponse<Category>> {
74
- return this.create<CreateCategoryRequest, Category>('/categories', data);
75
- }
85
+ async createCategory(
86
+ siteName: string,
87
+ data: CreateCategoryRequest,
88
+ csrfToken?: string
89
+ ): Promise<ApiResponse<{ message: string; category_id: number }>> {
90
+ const endpoint = this.siteScopedEndpoint(siteName, '/categories', { includeSitesSegment: false });
91
+ const path = this.buildPath(endpoint);
76
92
 
77
- /**
78
- * Update category
79
- */
80
- async updateCategory(id: number, data: Partial<CreateCategoryRequest>): Promise<ApiResponse<Category>> {
81
- return this.update<Partial<CreateCategoryRequest>, Category>(`/categories/${id}`, data);
82
- }
93
+ const result = await this.http.post<{ message: string; category_id: number }>(path, data, { csrfToken });
83
94
 
84
- /**
85
- * Delete category
86
- */
87
- async deleteCategory(id: number): Promise<ApiResponse<{ message: string }>> {
88
- return this.delete<{ message: string }>(`/categories/${id}`);
89
- }
95
+ // Invalidate cache after creation
96
+ if (this.cache) {
97
+ await this.cache.invalidate({ tags: this.buildCategoryTags(siteName) });
98
+ }
90
99
 
91
- /**
92
- * Get category tree (hierarchical structure)
93
- */
94
- async getCategoryTree(rootId?: number): Promise<ApiResponse<Array<Category & {
95
- children: Category[];
96
- }>>> {
97
- const endpoint = rootId ? `/categories/tree/${rootId}` : '/categories/tree';
98
- return this.getSingle(endpoint);
100
+ return result;
99
101
  }
100
102
 
101
103
  /**
102
- * Get category children
104
+ * Create new product category for a site
103
105
  */
104
- async getCategoryChildren(id: number): Promise<ApiResponse<Category[]>> {
105
- return this.getSingle<Category[]>(`/categories/${id}/children`);
106
- }
106
+ async createProductCategory(
107
+ siteName: string,
108
+ data: Omit<CreateCategoryRequest, 'category_type'>,
109
+ csrfToken?: string
110
+ ): Promise<ApiResponse<{ message: string; category_id: number }>> {
111
+ const endpoint = this.siteScopedEndpoint(siteName, '/categories/product', { includeSitesSegment: false });
112
+ const path = this.buildPath(endpoint);
107
113
 
108
- /**
109
- * Get category parent
110
- */
111
- async getCategoryParent(id: number): Promise<ApiResponse<Category | null>> {
112
- return this.getSingle<Category | null>(`/categories/${id}/parent`);
113
- }
114
+ const result = await this.http.post<{ message: string; category_id: number }>(path, data, { csrfToken });
114
115
 
115
- /**
116
- * Move category to new parent
117
- */
118
- async moveCategoryToParent(id: number, parentId: number | null): Promise<ApiResponse<Category>> {
119
- return this.patch<{ parentId: number | null }, Category>(`/categories/${id}`, { parentId });
120
- }
116
+ // Invalidate cache after creation
117
+ if (this.cache) {
118
+ await this.cache.invalidate({ tags: this.buildCategoryTags(siteName, 'categories:product') });
119
+ }
121
120
 
122
- /**
123
- * Get category breadcrumb path
124
- */
125
- async getCategoryBreadcrumb(id: number): Promise<ApiResponse<Array<{
126
- id: number;
127
- name: string;
128
- slug: string;
129
- }>>> {
130
- return this.getSingle(`/categories/${id}/breadcrumb`);
121
+ return result;
131
122
  }
132
123
 
133
124
  /**
134
- * Get category content/products
125
+ * Update category (validates it belongs to the site)
135
126
  */
136
- async getCategoryContent(id: number, params?: {
137
- page?: number;
138
- limit?: number;
139
- type?: 'content' | 'products' | 'all';
140
- }): Promise<ApiResponse<{
141
- content: any[];
142
- products: any[];
143
- total: number;
144
- }>> {
145
- return this.http.get(`/categories/${id}/content`, params);
127
+ async updateCategory(
128
+ siteName: string,
129
+ id: number,
130
+ data: Partial<CreateCategoryRequest>,
131
+ csrfToken?: string
132
+ ): Promise<ApiResponse<{ message: string }>> {
133
+ const endpoint = this.siteScopedEndpoint(siteName, `/categories/${id}`, { includeSitesSegment: false });
134
+ const path = this.buildPath(endpoint);
135
+
136
+ const result = await this.http.put<{ message: string }>(path, data, { csrfToken });
137
+
138
+ // Invalidate cache after update
139
+ if (this.cache) {
140
+ await this.cache.invalidate({ tags: this.buildCategoryTags(siteName, `categories:id:${id}`) });
141
+ }
142
+
143
+ return result;
146
144
  }
147
145
 
148
146
  /**
149
- * Bulk update category order
147
+ * Delete category (validates it belongs to the site)
150
148
  */
151
- async updateCategoryOrder(updates: Array<{
152
- id: number;
153
- order: number;
154
- parentId?: number;
155
- }>): Promise<ApiResponse<{ message: string }>> {
156
- return this.create('/categories/reorder', { updates });
149
+ async deleteCategory(siteName: string, id: number, csrfToken?: string): Promise<ApiResponse<{ message: string }>> {
150
+ const endpoint = this.siteScopedEndpoint(siteName, `/categories/${id}`, { includeSitesSegment: false });
151
+ const path = this.buildPath(endpoint);
152
+
153
+ const result = await this.http.delete<{ message: string }>(path, { csrfToken });
154
+
155
+ // Invalidate cache after deletion
156
+ if (this.cache) {
157
+ await this.cache.invalidate({ tags: this.buildCategoryTags(siteName) });
158
+ }
159
+
160
+ return result;
157
161
  }
158
162
 
159
163
  /**
160
- * Search categories
164
+ * Associate pages or products with categories
161
165
  */
162
- async searchCategories(query: string, params?: {
163
- limit?: number;
164
- organizationId?: number;
165
- }): Promise<ApiResponse<Category[]>> {
166
- return this.http.get(`/categories/search`, { q: query, ...params });
166
+ async associateCategories(
167
+ siteName: string,
168
+ data: {
169
+ entity_id: number;
170
+ entity_type: 'page' | 'product';
171
+ category_ids: number[];
172
+ },
173
+ csrfToken?: string
174
+ ): Promise<ApiResponse<{ message: string }>> {
175
+ const endpoint = this.siteScopedEndpoint(siteName, '/categories/associate', { includeSitesSegment: false });
176
+ const path = this.buildPath(endpoint);
177
+
178
+ const result = await this.http.post<{ message: string }>(path, data, { csrfToken });
179
+
180
+ // Invalidate cache after association
181
+ if (this.cache) {
182
+ await this.cache.invalidate({ tags: this.buildCategoryTags(siteName) });
183
+ }
184
+
185
+ return result;
167
186
  }
168
187
 
169
- private buildCategoryTags(siteName: string, slug: string): string[] {
188
+ private buildCategoryTags(siteName: string, extraTag?: string): string[] {
170
189
  const tags = new Set<string>(['categories']);
171
190
  if (siteName) {
172
191
  tags.add(`categories:site:${siteName}`);
173
192
  }
174
- if (slug) {
175
- tags.add(`categories:product:${siteName}:${slug}`);
193
+ if (extraTag) {
194
+ tags.add(extraTag);
176
195
  }
177
196
  return Array.from(tags.values());
178
197
  }
@@ -638,6 +638,7 @@ export interface SiteUserOrder {
638
638
  export interface RequestOtpRequest {
639
639
  email: string;
640
640
  waitlist?: boolean; // Mark user as waitlist signup
641
+ metadata?: Record<string, any>; // Optional metadata to set on user creation/update
641
642
  }
642
643
 
643
644
  export interface VerifyOtpRequest {