@thorprovider/adapters 4.2.1 → 4.3.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/PROVIDER_IMPLEMENTATION_ANALYSIS.md +659 -0
- package/dist/index.d.mts +160 -1
- package/dist/index.d.ts +160 -1
- package/dist/index.js +384 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +384 -2
- package/dist/index.mjs.map +1 -1
- package/docs/RETRY_SOLUTION_SUMMARY.md +26 -11
- package/docs/authentication.md +115 -62
- package/docs/capabilities-system.md +4 -2
- package/docs/category-customization.md +37 -29
- package/docs/fetch-reliability-solution.md +127 -278
- package/docs/getting-started.md +43 -29
- package/docs/profile-management.md +68 -38
- package/docs/services-overview.md +4 -2
- package/package.json +1 -1
- package/quick-reference.md +33 -0
- package/src/providers/medusa/admin.test.ts +404 -0
- package/src/providers/medusa/admin.ts +442 -0
- package/src/providers/medusa/auth.test.ts +137 -0
- package/src/providers/medusa/auth.ts +96 -1
- package/src/providers/medusa/index.ts +21 -1
- package/src/providers/shopify/auth.ts +6 -0
- package/src/resilience/README.md +70 -48
- package/src/core/errors.js +0 -161
- package/src/core/fetch-utils.js +0 -240
- package/src/core/logger.js +0 -63
- package/src/metadata/channel-scope-validation.js +0 -136
- package/src/metadata/channel-scope.js +0 -174
- package/src/metadata/index.js +0 -31
- package/src/providers/medusa/auth.js +0 -601
- package/src/providers/medusa/index.js +0 -2485
- package/src/providers/medusa/payment.js +0 -265
- package/src/providers/medusa/transforms.js +0 -505
- package/src/types/capabilities.types.js +0 -141
- package/src/types/provider.js +0 -9
- package/src/utils/payment-utils.js +0 -152
|
@@ -1,26 +1,32 @@
|
|
|
1
|
-
# Profile Management
|
|
2
|
-
|
|
3
1
|
---
|
|
2
|
+
title: Profile Management
|
|
3
|
+
purpose: Guide for managing customer profiles using capability-aware patterns for cross-backend compatibility
|
|
4
4
|
type: how-to
|
|
5
5
|
tags: [profile, account, customer, metadata, avatar]
|
|
6
6
|
related:
|
|
7
7
|
- authentication.md
|
|
8
8
|
- capabilities-system.md
|
|
9
9
|
- services-overview.md
|
|
10
|
-
updated: 2026-
|
|
10
|
+
updated: 2026-05-03
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
# Profile Management
|
|
14
|
+
|
|
13
15
|
Guide for managing customer profiles using capability-aware patterns for cross-backend compatibility.
|
|
14
16
|
|
|
15
17
|
## Prerequisites
|
|
16
18
|
|
|
17
19
|
- [Authentication set up](authentication.md)
|
|
18
|
-
- User logged in
|
|
20
|
+
- User logged in (`commerce.auth` available and `isAuthenticated()` returns `true`)
|
|
19
21
|
|
|
20
22
|
## Quick Example
|
|
21
23
|
|
|
22
24
|
```typescript
|
|
23
|
-
import {
|
|
25
|
+
import { createProvider } from '@thorprovider/adapters';
|
|
26
|
+
|
|
27
|
+
const commerce = createProvider({ provider: 'medusa', config: { ... } });
|
|
28
|
+
|
|
29
|
+
if (!commerce.auth) throw new Error('Auth not available');
|
|
24
30
|
|
|
25
31
|
// Simple profile update
|
|
26
32
|
const updated = await commerce.auth.updateCustomer({
|
|
@@ -41,6 +47,10 @@ const { updateEmail, updateProfile, deleteAccount } = commerce.capabilities;
|
|
|
41
47
|
if (updateEmail) {
|
|
42
48
|
// Render email update field
|
|
43
49
|
}
|
|
50
|
+
|
|
51
|
+
if (updateProfile) {
|
|
52
|
+
// Render name/phone fields
|
|
53
|
+
}
|
|
44
54
|
```
|
|
45
55
|
|
|
46
56
|
### Step 2: Retrieve Current Profile
|
|
@@ -48,6 +58,8 @@ if (updateEmail) {
|
|
|
48
58
|
Fetch the customer object to populate your forms.
|
|
49
59
|
|
|
50
60
|
```typescript
|
|
61
|
+
if (!commerce.auth) return;
|
|
62
|
+
|
|
51
63
|
const customer = await commerce.auth.getCurrentCustomer();
|
|
52
64
|
```
|
|
53
65
|
|
|
@@ -56,18 +68,24 @@ const customer = await commerce.auth.getCurrentCustomer();
|
|
|
56
68
|
Handle updates carefully, checking for specific field support.
|
|
57
69
|
|
|
58
70
|
```typescript
|
|
59
|
-
async function handleUpdate(formData
|
|
60
|
-
|
|
61
|
-
|
|
71
|
+
async function handleUpdate(formData: {
|
|
72
|
+
firstName: string;
|
|
73
|
+
lastName: string;
|
|
74
|
+
email?: string;
|
|
75
|
+
}) {
|
|
76
|
+
if (!commerce.auth) throw new Error('Auth not available');
|
|
77
|
+
|
|
78
|
+
const updateData: Record<string, unknown> = {};
|
|
79
|
+
|
|
62
80
|
if (commerce.capabilities.updateProfile) {
|
|
63
81
|
updateData.first_name = formData.firstName;
|
|
64
82
|
updateData.last_name = formData.lastName;
|
|
65
83
|
}
|
|
66
|
-
|
|
67
|
-
if (commerce.capabilities.updateEmail) {
|
|
84
|
+
|
|
85
|
+
if (commerce.capabilities.updateEmail && formData.email) {
|
|
68
86
|
updateData.email = formData.email;
|
|
69
87
|
}
|
|
70
|
-
|
|
88
|
+
|
|
71
89
|
return await commerce.auth.updateCustomer(updateData);
|
|
72
90
|
}
|
|
73
91
|
```
|
|
@@ -78,42 +96,54 @@ Handle password changes based on whether the backend supports direct or email-ba
|
|
|
78
96
|
|
|
79
97
|
```typescript
|
|
80
98
|
if (commerce.capabilities.changePasswordDirect) {
|
|
99
|
+
// Direct password change (Shopify, Spree, Magento)
|
|
81
100
|
await commerce.auth.updateCustomer({
|
|
82
|
-
|
|
83
|
-
password: 'new',
|
|
101
|
+
password: 'NewPassword123!',
|
|
84
102
|
});
|
|
103
|
+
} else if (commerce.capabilities.changePasswordViaEmail) {
|
|
104
|
+
// Email-based reset (Medusa v2)
|
|
105
|
+
await commerce.auth.requestPasswordReset(customer.email);
|
|
85
106
|
}
|
|
86
107
|
```
|
|
87
108
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
1. **Check Capabilities First:** Avoid rendering buttons for features the backend doesn't support.
|
|
91
|
-
2. **Use Metadata for Extensions:** Store extra info like `avatar_url` or `notifications` in `customer.metadata`.
|
|
92
|
-
3. **Handle Errors explicitly:** Distinguish between `EMAIL_TAKEN` and generic update errors.
|
|
93
|
-
4. **Validate on Client:** Use patterns/regex for phone and email before sending to the adapter.
|
|
94
|
-
5. **Protect UI with Loading States:** Backend operations can take time; show a spinner.
|
|
95
|
-
|
|
96
|
-
## ❌ DONT's
|
|
109
|
+
### Step 5: Account Deletion
|
|
97
110
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
111
|
+
```typescript
|
|
112
|
+
if (commerce.capabilities.deleteAccount && commerce.auth) {
|
|
113
|
+
await commerce.auth.deleteCustomerAccount();
|
|
114
|
+
}
|
|
115
|
+
```
|
|
103
116
|
|
|
104
|
-
##
|
|
117
|
+
## Avatar Management
|
|
105
118
|
|
|
106
|
-
|
|
107
|
-
Since many backends don't support native avatars, use an external service (S3/Cloudinary) and store the URL in metadata.
|
|
119
|
+
Most backends don't support native avatar uploads. Use an external service (S3, Cloudinary) and store the URL in metadata.
|
|
108
120
|
|
|
109
121
|
```typescript
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
122
|
+
// Check capability first
|
|
123
|
+
if (commerce.capabilities.avatarViaMetadata && commerce.auth) {
|
|
124
|
+
await commerce.auth.updateCustomer({
|
|
125
|
+
metadata: { avatar_url: 'https://cdn.example.com/avatars/user-123.jpg' },
|
|
126
|
+
});
|
|
127
|
+
}
|
|
113
128
|
```
|
|
114
129
|
|
|
115
|
-
|
|
116
|
-
|
|
130
|
+
> ⚠️ **Backend Disparity:** `uploadAvatar` (native file upload) is `false` for all current providers. Use `avatarViaMetadata` instead.
|
|
131
|
+
|
|
132
|
+
## ✅ DO's
|
|
133
|
+
|
|
134
|
+
1. **Check `commerce.capabilities` first:** Avoid rendering buttons for features the backend doesn't support.
|
|
135
|
+
2. **Use metadata for extensions:** Store extra info like `avatar_url` or notification preferences in `customer.metadata`.
|
|
136
|
+
3. **Handle errors explicitly:** Distinguish between `EMAIL_TAKEN` and generic update errors.
|
|
137
|
+
4. **Validate on client:** Use patterns/regex for phone and email before sending to the adapter.
|
|
138
|
+
5. **Re-fetch after update:** After a successful update, refresh the local customer state.
|
|
139
|
+
|
|
140
|
+
## ❌ DONT's
|
|
141
|
+
|
|
142
|
+
1. **Don't assume all backends are equal:** Medusa v2 has different capabilities than Shopify or WooCommerce.
|
|
143
|
+
2. **Don't hardcode profile fields:** Use the `UpdateCustomerData` type from `@thorprovider/adapters`.
|
|
144
|
+
3. **Don't ignore account deletion:** Provide a way for users to delete their account if `deleteAccount` is `true`.
|
|
145
|
+
4. **Don't send sensitive data in metadata:** Metadata is often visible to the user and may be less secure.
|
|
146
|
+
5. **Don't call `commerce.auth.updateCustomer` without checking `commerce.auth`:** It may be undefined.
|
|
117
147
|
|
|
118
148
|
## Common Errors & Solutions
|
|
119
149
|
|
|
@@ -128,16 +158,16 @@ Render only the fields supported by the current provider.
|
|
|
128
158
|
## Common Pitfalls
|
|
129
159
|
|
|
130
160
|
> ⚠️ **Pitfall 1: Metadata Overwrites**
|
|
131
|
-
> Some backends overwrite the entire metadata object instead of merging. Always fetch the current
|
|
161
|
+
> Some backends overwrite the entire metadata object instead of merging. Always fetch the current metadata first if you want to preserve existing fields.
|
|
132
162
|
|
|
133
163
|
> ⚠️ **Pitfall 2: Password Complexity**
|
|
134
|
-
> Backends have different password requirements. Let the backend validate and
|
|
164
|
+
> Backends have different password requirements. Let the backend validate and surface the error message.
|
|
135
165
|
|
|
136
166
|
## Troubleshooting
|
|
137
167
|
|
|
138
168
|
| Issue | Solution |
|
|
139
169
|
|-------|----------|
|
|
140
|
-
| Profile data not saving | Check if the user session is still active |
|
|
170
|
+
| Profile data not saving | Check if the user session is still active (`isAuthenticated()`) |
|
|
141
171
|
| Metadata field missing | Ensure the key exists in the backend's allowed metadata list |
|
|
142
172
|
| 403 Forbidden | User might not have permission to update their own profile |
|
|
143
173
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Services Overview
|
|
2
|
-
|
|
3
1
|
---
|
|
2
|
+
title: Services Overview
|
|
3
|
+
purpose: Overview of adapter services and provider capabilities
|
|
4
4
|
type: guide
|
|
5
5
|
tags: [architecture, services, commerce, api, architecture]
|
|
6
6
|
related:
|
|
@@ -10,6 +10,8 @@ related:
|
|
|
10
10
|
updated: 2026-02-28
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
# Services Overview
|
|
14
|
+
|
|
13
15
|
Overview of the functional services provided by Thor adapters and how they interact to provide a unified e-commerce interface.
|
|
14
16
|
|
|
15
17
|
## Overview
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thorprovider/adapters",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"description": "Framework-agnostic commerce adapters for Thor Commerce ecosystem - Multi-platform support with unified API",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org",
|
package/quick-reference.md
CHANGED
|
@@ -52,6 +52,39 @@ Framework-agnostic business logic SDK for e-commerce operations. Provides unifie
|
|
|
52
52
|
- **Collections**: Product collections/categories
|
|
53
53
|
- **Customer**: Authentication, profile management
|
|
54
54
|
- **Orders**: Order history, details, tracking
|
|
55
|
+
- **Auth — Multi-Tenant**: `linkCustomerToChannel()` — links authenticated customer to sales channel (Medusa only)
|
|
56
|
+
|
|
57
|
+
### Admin Client (Medusa only)
|
|
58
|
+
`provider.admin` — available when `adminConfig.apiKey` is set. Exposes 15 methods for the `/admin/thor/` endpoints:
|
|
59
|
+
```typescript
|
|
60
|
+
const commerce = createProvider({ provider: 'medusa', config: { ..., adminConfig: { apiKey: '...' } } })
|
|
61
|
+
if (!commerce.admin) throw new Error('Admin not configured')
|
|
62
|
+
|
|
63
|
+
// Sales Channel — Customers / API Keys / Categories
|
|
64
|
+
await commerce.admin.getChannelCustomers(salesChannelId, { limit, offset })
|
|
65
|
+
await commerce.admin.getChannelApiKeys(salesChannelId)
|
|
66
|
+
await commerce.admin.getChannelCategories(salesChannelId, { limit, offset, include_descendants })
|
|
67
|
+
|
|
68
|
+
// Category ↔ Sales Channel assignments
|
|
69
|
+
await commerce.admin.getCategorySalesChannels(categoryId)
|
|
70
|
+
await commerce.admin.assignCategoryToChannels(categoryId, { sales_channel_ids: [...] })
|
|
71
|
+
await commerce.admin.unassignCategoryFromChannels(categoryId, { sales_channel_ids: [...] })
|
|
72
|
+
|
|
73
|
+
// Collection ↔ Sales Channel assignments
|
|
74
|
+
await commerce.admin.getCollectionSalesChannels(collectionId)
|
|
75
|
+
await commerce.admin.assignCollectionToChannels(collectionId, { sales_channel_ids: [...] })
|
|
76
|
+
await commerce.admin.unassignCollectionFromChannels(collectionId, { sales_channel_ids: [...] })
|
|
77
|
+
|
|
78
|
+
// Storefront Config (visual theme, SEO defaults)
|
|
79
|
+
await commerce.admin.getStorefrontConfig(salesChannelId)
|
|
80
|
+
await commerce.admin.updateStorefrontConfig(salesChannelId, { theme_accent_color, logo_url, currency_code, seo_defaults })
|
|
81
|
+
|
|
82
|
+
// Site Designer Config (page builder config with history)
|
|
83
|
+
await commerce.admin.getSiteConfig(salesChannelId)
|
|
84
|
+
await commerce.admin.updateSiteConfig(salesChannelId, { config: { ... } })
|
|
85
|
+
await commerce.admin.getSiteConfigHistory(salesChannelId, { limit, offset })
|
|
86
|
+
await commerce.admin.restoreSiteConfig(salesChannelId, version)
|
|
87
|
+
```
|
|
55
88
|
|
|
56
89
|
### Provider Architecture
|
|
57
90
|
```
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tests for MedusaAdminClient
|
|
3
|
+
*
|
|
4
|
+
* Uses msw to intercept fetch calls and verify that each method:
|
|
5
|
+
* - Hits the correct URL and HTTP method
|
|
6
|
+
* - Sends `x-medusa-access-token`
|
|
7
|
+
* - Forwards query-string options correctly
|
|
8
|
+
* - Returns the parsed response body
|
|
9
|
+
* - Throws ProviderAPIError on non-2xx responses
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, expect, it } from 'vitest'
|
|
13
|
+
import { http, HttpResponse } from 'msw'
|
|
14
|
+
import { server } from '../stelorder/client.test-helpers'
|
|
15
|
+
import { MedusaAdminClient } from './admin'
|
|
16
|
+
|
|
17
|
+
const BASE = 'https://medusa.example.com'
|
|
18
|
+
const API_KEY = 'test-admin-key'
|
|
19
|
+
|
|
20
|
+
function client() {
|
|
21
|
+
return new MedusaAdminClient({ baseUrl: BASE, apiKey: API_KEY })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Helpers
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
function requireAdminKey(request: Request) {
|
|
29
|
+
expect(request.headers.get('x-medusa-access-token')).toBe(API_KEY)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Sales Channel — Customers
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
describe('MedusaAdminClient.getChannelCustomers', () => {
|
|
37
|
+
it('GET /admin/thor/sales-channels/:id/customers with defaults', async () => {
|
|
38
|
+
server.use(
|
|
39
|
+
http.get(`${BASE}/admin/thor/sales-channels/sc_01/customers`, ({ request }) => {
|
|
40
|
+
requireAdminKey(request)
|
|
41
|
+
const url = new URL(request.url)
|
|
42
|
+
expect(url.searchParams.get('limit')).toBe('20')
|
|
43
|
+
expect(url.searchParams.get('offset')).toBe('0')
|
|
44
|
+
return HttpResponse.json({ customers: [{ id: 'cus_1', email: 'a@b.com', sales_channel_ids: ['sc_01'] }], count: 1 })
|
|
45
|
+
}),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
const result = await client().getChannelCustomers('sc_01')
|
|
49
|
+
expect(result.customers).toHaveLength(1)
|
|
50
|
+
expect(result.count).toBe(1)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('forwards custom limit/offset', async () => {
|
|
54
|
+
server.use(
|
|
55
|
+
http.get(`${BASE}/admin/thor/sales-channels/sc_01/customers`, ({ request }) => {
|
|
56
|
+
const url = new URL(request.url)
|
|
57
|
+
expect(url.searchParams.get('limit')).toBe('5')
|
|
58
|
+
expect(url.searchParams.get('offset')).toBe('10')
|
|
59
|
+
return HttpResponse.json({ customers: [], count: 0 })
|
|
60
|
+
}),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
await client().getChannelCustomers('sc_01', { limit: 5, offset: 10 })
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Sales Channel — API Keys
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
describe('MedusaAdminClient.getChannelApiKeys', () => {
|
|
72
|
+
it('GET /admin/thor/sales-channels/:id/api-keys', async () => {
|
|
73
|
+
server.use(
|
|
74
|
+
http.get(`${BASE}/admin/thor/sales-channels/sc_01/api-keys`, ({ request }) => {
|
|
75
|
+
requireAdminKey(request)
|
|
76
|
+
return HttpResponse.json({ api_keys: [{ id: 'apk_1', token: 'pk_live_xxx', sales_channel_id: 'sc_01' }] })
|
|
77
|
+
}),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
const result = await client().getChannelApiKeys('sc_01')
|
|
81
|
+
expect(result.api_keys).toHaveLength(1)
|
|
82
|
+
expect(result.api_keys[0].id).toBe('apk_1')
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Sales Channel — Categories
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
describe('MedusaAdminClient.getChannelCategories', () => {
|
|
91
|
+
it('GET /admin/thor/sales-channels/:id/categories with include_descendants', async () => {
|
|
92
|
+
server.use(
|
|
93
|
+
http.get(`${BASE}/admin/thor/sales-channels/sc_01/categories`, ({ request }) => {
|
|
94
|
+
const url = new URL(request.url)
|
|
95
|
+
expect(url.searchParams.get('include_descendants')).toBe('true')
|
|
96
|
+
return HttpResponse.json({ categories: [], count: 0 })
|
|
97
|
+
}),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
const result = await client().getChannelCategories('sc_01', { include_descendants: true })
|
|
101
|
+
expect(result.categories).toHaveLength(0)
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Category — Sales Channels
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
describe('MedusaAdminClient.getCategorySalesChannels', () => {
|
|
110
|
+
it('GET /admin/thor/categories/:id/sales-channels', async () => {
|
|
111
|
+
server.use(
|
|
112
|
+
http.get(`${BASE}/admin/thor/categories/cat_01/sales-channels`, ({ request }) => {
|
|
113
|
+
requireAdminKey(request)
|
|
114
|
+
return HttpResponse.json({ sales_channels: [{ id: 'sc_01', name: 'Main' }] })
|
|
115
|
+
}),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
const result = await client().getCategorySalesChannels('cat_01')
|
|
119
|
+
expect(result.sales_channels).toHaveLength(1)
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Category — Assign / Unassign
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
describe('MedusaAdminClient.assignCategoryToChannels', () => {
|
|
128
|
+
it('POST /admin/thor/categories/:id/sales-channels', async () => {
|
|
129
|
+
server.use(
|
|
130
|
+
http.post(`${BASE}/admin/thor/categories/cat_01/sales-channels`, async ({ request }) => {
|
|
131
|
+
requireAdminKey(request)
|
|
132
|
+
const body = await request.clone().json() as any
|
|
133
|
+
expect(body.sales_channel_ids).toEqual(['sc_01'])
|
|
134
|
+
return HttpResponse.json({ message: 'ok', linked: 1 })
|
|
135
|
+
}),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
const result = await client().assignCategoryToChannels('cat_01', { sales_channel_ids: ['sc_01'] })
|
|
139
|
+
expect(result.linked).toBe(1)
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
describe('MedusaAdminClient.unassignCategoryFromChannels', () => {
|
|
144
|
+
it('DELETE /admin/thor/categories/:id/sales-channels with body', async () => {
|
|
145
|
+
server.use(
|
|
146
|
+
http.delete(`${BASE}/admin/thor/categories/cat_01/sales-channels`, async ({ request }) => {
|
|
147
|
+
requireAdminKey(request)
|
|
148
|
+
const body = await request.clone().json() as any
|
|
149
|
+
expect(body.sales_channel_ids).toEqual(['sc_01'])
|
|
150
|
+
return HttpResponse.json({ message: 'ok', dismissed: 1 })
|
|
151
|
+
}),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
const result = await client().unassignCategoryFromChannels('cat_01', { sales_channel_ids: ['sc_01'] })
|
|
155
|
+
expect(result.dismissed).toBe(1)
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Collection — Sales Channels
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
describe('MedusaAdminClient.getCollectionSalesChannels', () => {
|
|
164
|
+
it('GET /admin/thor/collections/:id/sales-channels', async () => {
|
|
165
|
+
server.use(
|
|
166
|
+
http.get(`${BASE}/admin/thor/collections/col_01/sales-channels`, ({ request }) => {
|
|
167
|
+
requireAdminKey(request)
|
|
168
|
+
return HttpResponse.json({ sales_channels: [{ id: 'sc_01', name: 'Main' }] })
|
|
169
|
+
}),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
const result = await client().getCollectionSalesChannels('col_01')
|
|
173
|
+
expect(result.sales_channels).toHaveLength(1)
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
describe('MedusaAdminClient.assignCollectionToChannels', () => {
|
|
178
|
+
it('POST /admin/thor/collections/:id/sales-channels', async () => {
|
|
179
|
+
server.use(
|
|
180
|
+
http.post(`${BASE}/admin/thor/collections/col_01/sales-channels`, async ({ request }) => {
|
|
181
|
+
const body = await request.clone().json() as any
|
|
182
|
+
expect(body.sales_channel_ids).toEqual(['sc_01'])
|
|
183
|
+
return HttpResponse.json({ message: 'ok', linked: 1 })
|
|
184
|
+
}),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
const result = await client().assignCollectionToChannels('col_01', { sales_channel_ids: ['sc_01'] })
|
|
188
|
+
expect(result.linked).toBe(1)
|
|
189
|
+
})
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
describe('MedusaAdminClient.unassignCollectionFromChannels', () => {
|
|
193
|
+
it('DELETE /admin/thor/collections/:id/sales-channels', async () => {
|
|
194
|
+
server.use(
|
|
195
|
+
http.delete(`${BASE}/admin/thor/collections/col_01/sales-channels`, async ({ request }) => {
|
|
196
|
+
const body = await request.clone().json() as any
|
|
197
|
+
expect(body.sales_channel_ids).toEqual(['sc_01'])
|
|
198
|
+
return HttpResponse.json({ message: 'ok', dismissed: 1 })
|
|
199
|
+
}),
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
const result = await client().unassignCollectionFromChannels('col_01', { sales_channel_ids: ['sc_01'] })
|
|
203
|
+
expect(result.dismissed).toBe(1)
|
|
204
|
+
})
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// Storefront Config
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
describe('MedusaAdminClient.getStorefrontConfig', () => {
|
|
212
|
+
it('GET /admin/thor/storefront-config/:id', async () => {
|
|
213
|
+
server.use(
|
|
214
|
+
http.get(`${BASE}/admin/thor/storefront-config/sc_01`, ({ request }) => {
|
|
215
|
+
requireAdminKey(request)
|
|
216
|
+
return HttpResponse.json({
|
|
217
|
+
storefront_config: {
|
|
218
|
+
id: 'scfg_01',
|
|
219
|
+
sales_channel_id: 'sc_01',
|
|
220
|
+
theme_accent_color: '#000',
|
|
221
|
+
logo_url: '',
|
|
222
|
+
currency_code: 'USD',
|
|
223
|
+
seo_defaults: { title: 'Store', description: '' },
|
|
224
|
+
created_at: '2026-01-01T00:00:00Z',
|
|
225
|
+
updated_at: '2026-01-01T00:00:00Z',
|
|
226
|
+
},
|
|
227
|
+
})
|
|
228
|
+
}),
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
const result = await client().getStorefrontConfig('sc_01')
|
|
232
|
+
expect(result.storefront_config.id).toBe('scfg_01')
|
|
233
|
+
})
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
describe('MedusaAdminClient.updateStorefrontConfig', () => {
|
|
237
|
+
it('PUT /admin/thor/storefront-config/:id', async () => {
|
|
238
|
+
server.use(
|
|
239
|
+
http.put(`${BASE}/admin/thor/storefront-config/sc_01`, async ({ request }) => {
|
|
240
|
+
const body = await request.clone().json() as any
|
|
241
|
+
expect(body.theme_accent_color).toBe('#ff0000')
|
|
242
|
+
return HttpResponse.json({
|
|
243
|
+
storefront_config: {
|
|
244
|
+
id: 'scfg_01',
|
|
245
|
+
sales_channel_id: 'sc_01',
|
|
246
|
+
theme_accent_color: '#ff0000',
|
|
247
|
+
logo_url: '',
|
|
248
|
+
currency_code: 'USD',
|
|
249
|
+
seo_defaults: { title: 'Store', description: '' },
|
|
250
|
+
created_at: '2026-01-01T00:00:00Z',
|
|
251
|
+
updated_at: '2026-01-02T00:00:00Z',
|
|
252
|
+
},
|
|
253
|
+
})
|
|
254
|
+
}),
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
const result = await client().updateStorefrontConfig('sc_01', {
|
|
258
|
+
theme_accent_color: '#ff0000',
|
|
259
|
+
logo_url: '',
|
|
260
|
+
currency_code: 'USD',
|
|
261
|
+
seo_defaults: { title: 'Store', description: '' },
|
|
262
|
+
})
|
|
263
|
+
expect(result.storefront_config.theme_accent_color).toBe('#ff0000')
|
|
264
|
+
})
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
// Site Config
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
describe('MedusaAdminClient.getSiteConfig', () => {
|
|
272
|
+
it('GET /admin/thor/site-config/:sales_channel_id', async () => {
|
|
273
|
+
server.use(
|
|
274
|
+
http.get(`${BASE}/admin/thor/site-config/sc_01`, ({ request }) => {
|
|
275
|
+
requireAdminKey(request)
|
|
276
|
+
return HttpResponse.json({
|
|
277
|
+
site_config: {
|
|
278
|
+
id: 'scfg_01',
|
|
279
|
+
sales_channel_id: 'sc_01',
|
|
280
|
+
version: 'v1',
|
|
281
|
+
config: { theme: 'dark' },
|
|
282
|
+
created_at: '2026-01-01T00:00:00Z',
|
|
283
|
+
updated_at: '2026-01-01T00:00:00Z',
|
|
284
|
+
history: [],
|
|
285
|
+
},
|
|
286
|
+
})
|
|
287
|
+
}),
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
const result = await client().getSiteConfig('sc_01')
|
|
291
|
+
expect(result.site_config.id).toBe('scfg_01')
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
describe('MedusaAdminClient.updateSiteConfig', () => {
|
|
296
|
+
it('PUT /admin/thor/site-config/:sales_channel_id', async () => {
|
|
297
|
+
server.use(
|
|
298
|
+
http.put(`${BASE}/admin/thor/site-config/sc_01`, async ({ request }) => {
|
|
299
|
+
const body = await request.json() as any
|
|
300
|
+
expect(body.config).toEqual({ theme: 'light' })
|
|
301
|
+
return HttpResponse.json({
|
|
302
|
+
site_config: {
|
|
303
|
+
id: 'scfg_02',
|
|
304
|
+
sales_channel_id: 'sc_01',
|
|
305
|
+
version: 'v2',
|
|
306
|
+
config: { theme: 'light' },
|
|
307
|
+
created_at: '2026-01-01T00:00:00Z',
|
|
308
|
+
updated_at: '2026-01-02T00:00:00Z',
|
|
309
|
+
history: [],
|
|
310
|
+
},
|
|
311
|
+
})
|
|
312
|
+
}),
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
const result = await client().updateSiteConfig('sc_01', { config: { theme: 'light' } })
|
|
316
|
+
expect(result.site_config.version).toBe('v2')
|
|
317
|
+
})
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
describe('MedusaAdminClient.getSiteConfigHistory', () => {
|
|
321
|
+
it('GET /admin/thor/site-config/:id/history with defaults', async () => {
|
|
322
|
+
server.use(
|
|
323
|
+
http.get(`${BASE}/admin/thor/site-config/sc_01/history`, ({ request }) => {
|
|
324
|
+
requireAdminKey(request)
|
|
325
|
+
const url = new URL(request.url)
|
|
326
|
+
expect(url.searchParams.get('limit')).toBe('20')
|
|
327
|
+
expect(url.searchParams.get('offset')).toBe('0')
|
|
328
|
+
return HttpResponse.json({ history: [], count: 0 })
|
|
329
|
+
}),
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
const result = await client().getSiteConfigHistory('sc_01')
|
|
333
|
+
expect(result.count).toBe(0)
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
it('forwards custom limit/offset', async () => {
|
|
337
|
+
server.use(
|
|
338
|
+
http.get(`${BASE}/admin/thor/site-config/sc_01/history`, ({ request }) => {
|
|
339
|
+
const url = new URL(request.url)
|
|
340
|
+
expect(url.searchParams.get('limit')).toBe('5')
|
|
341
|
+
expect(url.searchParams.get('offset')).toBe('10')
|
|
342
|
+
return HttpResponse.json({ history: [], count: 0 })
|
|
343
|
+
}),
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
await client().getSiteConfigHistory('sc_01', { limit: 5, offset: 10 })
|
|
347
|
+
})
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
describe('MedusaAdminClient.restoreSiteConfig', () => {
|
|
351
|
+
it('POST /admin/thor/site-config/:id/restore/:version', async () => {
|
|
352
|
+
server.use(
|
|
353
|
+
http.post(`${BASE}/admin/thor/site-config/sc_01/restore/v1`, ({ request }) => {
|
|
354
|
+
requireAdminKey(request)
|
|
355
|
+
return HttpResponse.json({
|
|
356
|
+
site_config: {
|
|
357
|
+
id: 'scfg_03',
|
|
358
|
+
sales_channel_id: 'sc_01',
|
|
359
|
+
version: 'v3',
|
|
360
|
+
config: { theme: 'dark' },
|
|
361
|
+
created_at: '2026-01-01T00:00:00Z',
|
|
362
|
+
updated_at: '2026-01-03T00:00:00Z',
|
|
363
|
+
history: [],
|
|
364
|
+
},
|
|
365
|
+
})
|
|
366
|
+
}),
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
const result = await client().restoreSiteConfig('sc_01', 'v1')
|
|
370
|
+
expect(result.site_config.version).toBe('v3')
|
|
371
|
+
})
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
// ---------------------------------------------------------------------------
|
|
375
|
+
// Error handling
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
|
|
378
|
+
describe('MedusaAdminClient error handling', () => {
|
|
379
|
+
it('throws ProviderAPIError on 404', async () => {
|
|
380
|
+
server.use(
|
|
381
|
+
http.get(`${BASE}/admin/thor/sales-channels/sc_missing/customers`, () =>
|
|
382
|
+
HttpResponse.json({ message: 'Sales channel not found' }, { status: 404 }),
|
|
383
|
+
),
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
await expect(client().getChannelCustomers('sc_missing')).rejects.toMatchObject({
|
|
387
|
+
name: 'ProviderAPIError',
|
|
388
|
+
provider: 'ADMIN_API_404',
|
|
389
|
+
})
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
it('throws ProviderAPIError on 401', async () => {
|
|
393
|
+
server.use(
|
|
394
|
+
http.get(`${BASE}/admin/thor/sales-channels/sc_01/api-keys`, () =>
|
|
395
|
+
HttpResponse.json({ message: 'Unauthorized' }, { status: 401 }),
|
|
396
|
+
),
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
await expect(client().getChannelApiKeys('sc_01')).rejects.toMatchObject({
|
|
400
|
+
name: 'ProviderAPIError',
|
|
401
|
+
provider: 'ADMIN_API_401',
|
|
402
|
+
})
|
|
403
|
+
})
|
|
404
|
+
})
|