@thorprovider/adapters 4.2.0 → 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.
Files changed (56) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/PROVIDER_IMPLEMENTATION_ANALYSIS.md +659 -0
  3. package/README.md +19 -0
  4. package/dist/index.d.mts +249 -2
  5. package/dist/index.d.ts +249 -2
  6. package/dist/index.js +1450 -2
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +1448 -2
  9. package/dist/index.mjs.map +1 -1
  10. package/docs/RETRY_SOLUTION_SUMMARY.md +26 -11
  11. package/docs/authentication.md +115 -62
  12. package/docs/capabilities-system.md +34 -2
  13. package/docs/category-customization.md +37 -29
  14. package/docs/fetch-reliability-solution.md +127 -278
  15. package/docs/getting-started.md +43 -29
  16. package/docs/profile-management.md +68 -38
  17. package/docs/providers-overview.md +14 -0
  18. package/docs/services-overview.md +20 -2
  19. package/package.json +10 -6
  20. package/quick-reference.md +34 -0
  21. package/src/factory/index.ts +4 -0
  22. package/src/index.ts +3 -0
  23. package/src/providers/index.ts +3 -0
  24. package/src/providers/medusa/admin.test.ts +404 -0
  25. package/src/providers/medusa/admin.ts +442 -0
  26. package/src/providers/medusa/auth.test.ts +137 -0
  27. package/src/providers/medusa/auth.ts +96 -1
  28. package/src/providers/medusa/index.ts +21 -1
  29. package/src/providers/shopify/auth.ts +6 -0
  30. package/src/providers/stelorder/client.test-helpers.ts +3 -0
  31. package/src/providers/stelorder/client.test.ts +58 -0
  32. package/src/providers/stelorder/client.ts +326 -0
  33. package/src/providers/stelorder/index.test.ts +48 -0
  34. package/src/providers/stelorder/index.ts +685 -0
  35. package/src/providers/stelorder/live-smoke.test.ts +54 -0
  36. package/src/providers/stelorder/transforms.test.ts +103 -0
  37. package/src/providers/stelorder/transforms.ts +631 -0
  38. package/src/resilience/README.md +70 -48
  39. package/src/types/capabilities.types.ts +51 -0
  40. package/vitest.config.ts +13 -0
  41. package/vitest.setup.ts +6 -0
  42. package/.turbo/turbo-build.log +0 -18
  43. package/dist/.tsbuildinfo +0 -1
  44. package/src/core/errors.js +0 -161
  45. package/src/core/fetch-utils.js +0 -240
  46. package/src/core/logger.js +0 -63
  47. package/src/metadata/channel-scope-validation.js +0 -136
  48. package/src/metadata/channel-scope.js +0 -174
  49. package/src/metadata/index.js +0 -31
  50. package/src/providers/medusa/auth.js +0 -601
  51. package/src/providers/medusa/index.js +0 -2485
  52. package/src/providers/medusa/payment.js +0 -265
  53. package/src/providers/medusa/transforms.js +0 -505
  54. package/src/types/capabilities.types.js +0 -141
  55. package/src/types/provider.js +0 -9
  56. 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
+ })