@thorprovider/adapters 4.2.1 → 5.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.
Files changed (60) hide show
  1. package/PROVIDER_IMPLEMENTATION_ANALYSIS.md +659 -0
  2. package/dist/index.d.mts +225 -387
  3. package/dist/index.d.ts +225 -387
  4. package/dist/index.js +613 -3062
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +609 -3052
  7. package/dist/index.mjs.map +1 -1
  8. package/docs/RETRY_SOLUTION_SUMMARY.md +26 -11
  9. package/docs/authentication.md +115 -62
  10. package/docs/capabilities-system.md +4 -2
  11. package/docs/category-customization.md +37 -29
  12. package/docs/fetch-reliability-solution.md +127 -278
  13. package/docs/getting-started.md +43 -29
  14. package/docs/profile-management.md +68 -38
  15. package/docs/services-overview.md +4 -2
  16. package/package.json +1 -1
  17. package/quick-reference.md +33 -0
  18. package/src/backend/factory.ts +1 -11
  19. package/src/factory/index.ts +74 -52
  20. package/src/index.ts +5 -15
  21. package/src/providers/index.ts +1 -16
  22. package/src/providers/medusa/admin.test.ts +404 -0
  23. package/src/providers/medusa/admin.ts +442 -0
  24. package/src/providers/medusa/auth.test.ts +137 -0
  25. package/src/providers/medusa/auth.ts +96 -1
  26. package/src/providers/medusa/index.ts +21 -1
  27. package/src/resilience/README.md +70 -48
  28. package/src/types/capabilities.types.ts +0 -250
  29. package/src/core/errors.js +0 -161
  30. package/src/core/fetch-utils.js +0 -240
  31. package/src/core/logger.js +0 -63
  32. package/src/metadata/channel-scope-validation.js +0 -136
  33. package/src/metadata/channel-scope.js +0 -174
  34. package/src/metadata/index.js +0 -31
  35. package/src/providers/bigcommerce/bigcommerce.ts +0 -6
  36. package/src/providers/bigcommerce/index.ts +0 -315
  37. package/src/providers/bigcommerce/transforms.ts +0 -227
  38. package/src/providers/magento/index.ts +0 -387
  39. package/src/providers/magento/magento.ts +0 -5
  40. package/src/providers/magento/queries.ts +0 -267
  41. package/src/providers/magento/transforms.ts +0 -249
  42. package/src/providers/medusa/auth.js +0 -601
  43. package/src/providers/medusa/index.js +0 -2485
  44. package/src/providers/medusa/payment.js +0 -265
  45. package/src/providers/medusa/transforms.js +0 -505
  46. package/src/providers/shopify/auth.ts +0 -402
  47. package/src/providers/shopify/index.ts +0 -444
  48. package/src/providers/shopify/mutations.ts +0 -247
  49. package/src/providers/shopify/queries.ts +0 -324
  50. package/src/providers/shopify/shopify.ts +0 -7
  51. package/src/providers/shopify/transforms.ts +0 -275
  52. package/src/providers/spree/index.ts +0 -344
  53. package/src/providers/spree/spree.ts +0 -5
  54. package/src/providers/spree/transforms.ts +0 -254
  55. package/src/providers/woocommerce/index.ts +0 -310
  56. package/src/providers/woocommerce/transforms.ts +0 -254
  57. package/src/providers/woocommerce/woocommerce.ts +0 -5
  58. package/src/types/capabilities.types.js +0 -141
  59. package/src/types/provider.js +0 -9
  60. package/src/utils/payment-utils.js +0 -152
@@ -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
+ })