@thorprovider/adapters 5.0.0 → 5.0.1

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 (47) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +6 -0
  3. package/README.md +53 -631
  4. package/dist/index.d.mts +406 -194
  5. package/dist/index.d.ts +406 -194
  6. package/dist/index.js +3092 -604
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +3081 -602
  9. package/dist/index.mjs.map +1 -1
  10. package/docs/RETRY_SOLUTION_SUMMARY.md +1 -1
  11. package/docs/authentication.md +0 -26
  12. package/docs/capabilities-system.md +66 -36
  13. package/docs/getting-started.md +1 -1
  14. package/docs/providers-overview.md +16 -13
  15. package/docs/update-documentation.md +10 -8
  16. package/package.json +6 -5
  17. package/quick-reference.md +0 -33
  18. package/src/backend/factory.ts +11 -1
  19. package/src/factory/index.ts +52 -74
  20. package/src/index.ts +17 -3
  21. package/src/providers/bigcommerce/bigcommerce.ts +6 -0
  22. package/src/providers/bigcommerce/index.ts +315 -0
  23. package/src/providers/bigcommerce/transforms.ts +227 -0
  24. package/src/providers/index.ts +16 -1
  25. package/src/providers/magento/index.ts +387 -0
  26. package/src/providers/magento/magento.ts +5 -0
  27. package/src/providers/magento/queries.ts +267 -0
  28. package/src/providers/magento/transforms.ts +249 -0
  29. package/src/providers/medusa/auth.test.ts +94 -92
  30. package/src/providers/medusa/auth.ts +72 -123
  31. package/src/providers/medusa/index.test.ts +392 -0
  32. package/src/providers/medusa/index.ts +1 -21
  33. package/src/providers/medusa/live-smoke.test.ts +412 -0
  34. package/src/providers/shopify/auth.ts +402 -0
  35. package/src/providers/shopify/index.ts +444 -0
  36. package/src/providers/shopify/mutations.ts +247 -0
  37. package/src/providers/shopify/queries.ts +324 -0
  38. package/src/providers/shopify/shopify.ts +7 -0
  39. package/src/providers/shopify/transforms.ts +275 -0
  40. package/src/providers/spree/index.ts +344 -0
  41. package/src/providers/spree/spree.ts +5 -0
  42. package/src/providers/spree/transforms.ts +254 -0
  43. package/src/providers/woocommerce/index.ts +310 -0
  44. package/src/providers/woocommerce/transforms.ts +254 -0
  45. package/src/providers/woocommerce/woocommerce.ts +5 -0
  46. package/src/types/capabilities.types.ts +250 -0
  47. package/vitest.live.config.ts +18 -0
@@ -49,7 +49,6 @@ import { validatePaymentId } from '../../utils/payment-utils';
49
49
  import { transformProduct, transformCart, transformCollection, transformCategory, transformOrder, transformCustomer, transformStockLocation, transformInventoryLevel, transformAddress, transformStorefrontConfig } from './transforms';
50
50
  import { MedusaAuthProvider } from './auth';
51
51
  import { getPaymentMethods } from './payment';
52
- import { MedusaAdminClient } from './admin';
53
52
  import {
54
53
  buildChannelScopeMetadata,
55
54
  filterByChannel,
@@ -164,19 +163,6 @@ export class MedusaProvider implements CommerceProvider {
164
163
  * Only created when adminConfig.apiKey is provided.
165
164
  */
166
165
  private sdkAdmin?: Medusa;
167
-
168
- /**
169
- * Admin client for Thor multi-tenant endpoints (`/admin/thor/`).
170
- * Available when `adminConfig.apiKey` is provided.
171
- *
172
- * @example
173
- * ```typescript
174
- * const { customers } = await commerce.admin.getChannelCustomers('sc_01JXXXXX');
175
- * const { api_keys } = await commerce.admin.getChannelApiKeys('sc_01JXXXXX');
176
- * await commerce.admin.updateStorefrontConfig('sc_01JXXXXX', { theme_accent_color: '#EF4444', ... });
177
- * ```
178
- */
179
- public readonly admin?: MedusaAdminClient;
180
166
  /** Cached distinct product status values — rarely changes, cached for process lifetime */
181
167
  private _cachedProductStatuses: string[] | null = null;
182
168
 
@@ -267,18 +253,12 @@ export class MedusaProvider implements CommerceProvider {
267
253
  debug: config.debug ?? false,
268
254
  // No publishableKey or auth config: this instance is admin-only, no JWT needed
269
255
  });
270
- // Admin client for Thor multi-tenant custom endpoints (/admin/thor/)
271
- (this as any).admin = new MedusaAdminClient({
272
- baseUrl: config.baseUrl,
273
- apiKey: config.adminConfig.apiKey,
274
- debug: config.debug,
275
- });
276
256
  this.logger.info('[Admin Auth] Dedicated admin SDK instance created with API key');
277
257
  }
278
258
 
279
259
  // Initialize auth provider if config is provided
280
260
  if (config.auth) {
281
- this.auth = new MedusaAuthProvider(this.sdk, config.auth, config.debug, config.baseUrl, config.publishableKey);
261
+ this.auth = new MedusaAuthProvider(this.sdk, config.auth, config.debug);
282
262
  this.logger.info('Auth provider initialized');
283
263
  }
284
264
 
@@ -0,0 +1,412 @@
1
+ /**
2
+ * Medusa Provider — Live Smoke Tests
3
+ *
4
+ * Exercises the adapter against a real Medusa backend (dev.erpthor.com).
5
+ * Guarded by MEDUSA_SMOKE_TEST=1 environment variable.
6
+ *
7
+ * Environment variables (all required when MEDUSA_SMOKE_TEST=1):
8
+ * MEDUSA_BASE_URL - Backend URL (e.g. https://dev.erpthor.com)
9
+ * MEDUSA_PUBLISHABLE_KEY - Publishable API key
10
+ * MEDUSA_SECRET_KEY - Secret admin API key (sk_...) for SDK Basic auth
11
+ * MEDUSA_SALES_CHANNEL_ID - Sales channel ID for scoped queries
12
+ * MEDUSA_ADMIN_EMAIL - Admin email for JWT auth (required for Thor admin endpoints)
13
+ * MEDUSA_ADMIN_PASSWORD - Admin password
14
+ *
15
+ * Optional:
16
+ * MEDUSA_TEST_CUSTOMER_EMAIL - Existing customer email for auth tests
17
+ * MEDUSA_TEST_CUSTOMER_PASSWORD - Existing customer password
18
+ *
19
+ * Known issue: MedusaAdminClient uses x-medusa-access-token header which Medusa v2
20
+ * does not support. The backend requires Authorization: Bearer <JWT>. Group D tests
21
+ * work around this by obtaining a JWT via login and testing Thor admin endpoints directly.
22
+ */
23
+
24
+ import { describe, expect, it, beforeAll } from 'vitest'
25
+ import { MedusaProvider } from './index'
26
+ import { MedusaAdminClient } from './admin'
27
+
28
+ const runLive = process.env.MEDUSA_SMOKE_TEST === '1';
29
+
30
+ function requiredEnv(name: string): string {
31
+ const value = process.env[name];
32
+ if (!value) throw new Error(`Missing required env var: ${name}`);
33
+ return value;
34
+ }
35
+
36
+ function optionalEnv(name: string): string | undefined {
37
+ return process.env[name] || undefined;
38
+ }
39
+
40
+ if (!runLive) {
41
+ describe('Medusa live smoke', () => {
42
+ it.skip('disabled unless MEDUSA_SMOKE_TEST=1', () => {});
43
+ });
44
+ } else {
45
+ const baseUrl = requiredEnv('MEDUSA_BASE_URL');
46
+ const publishableKey = requiredEnv('MEDUSA_PUBLISHABLE_KEY');
47
+ const secretKey = requiredEnv('MEDUSA_SECRET_KEY');
48
+ const salesChannelId = requiredEnv('MEDUSA_SALES_CHANNEL_ID');
49
+ const adminEmail = requiredEnv('MEDUSA_ADMIN_EMAIL');
50
+ const adminPassword = requiredEnv('MEDUSA_ADMIN_PASSWORD');
51
+
52
+ // --------------------------------------------------------------------------
53
+ // Group A: Thor Plugin Store Endpoints (custom /store/thor/*)
54
+ // --------------------------------------------------------------------------
55
+ describe('Group A: Thor Store Plugin Endpoints', () => {
56
+ let provider: MedusaProvider;
57
+
58
+ beforeAll(() => {
59
+ provider = new MedusaProvider({
60
+ baseUrl,
61
+ publishableKey,
62
+ currencyCode: 'USD',
63
+ adminConfig: { apiKey: secretKey },
64
+ });
65
+ });
66
+
67
+ it('GET /store/thor/capabilities', async () => {
68
+ // capabilities is fetched in constructor, wait a tick then access
69
+ await new Promise((r) => setTimeout(r, 2000))
70
+ const caps = provider.capabilities
71
+ expect(caps).toBeDefined()
72
+ expect(caps).toHaveProperty('advancedSearch')
73
+ });
74
+
75
+ it('GET /store/thor/categories (channel-scoped)', async () => {
76
+ const categories = await provider.getCategories({ limit: 5 });
77
+ expect(categories).toBeInstanceOf(Array);
78
+ });
79
+
80
+ it('GET /store/thor/config (storefront config)', async () => {
81
+ const config = await provider.getStorefrontConfig();
82
+ // May return null if no config set for this channel — should not throw
83
+ expect(config === null || typeof config === 'object').toBe(true);
84
+ });
85
+
86
+ it('GET /store/thor/site-config (designer config)', async () => {
87
+ const config = await provider.getDesignerConfig();
88
+ expect(config === null || typeof config === 'object').toBe(true);
89
+ });
90
+
91
+ it('GET /store/search (advanced search)', async () => {
92
+ // Search depends on MeiliSearch being running on the backend — may 500 if not available
93
+ try {
94
+ const result = await provider.searchProductsAdvanced({ query: 'test', first: 3 })
95
+ expect(result).toHaveProperty('products')
96
+ expect(result.products).toBeInstanceOf(Array)
97
+ } catch (e: any) {
98
+ // MeiliSearch not running is acceptable in dev
99
+ console.log('Search endpoint unavailable (MeiliSearch down):', e.message?.slice(0, 80));
100
+ }
101
+ });
102
+ });
103
+
104
+ // --------------------------------------------------------------------------
105
+ // Group B: Standard Medusa Store Endpoints (via SDK)
106
+ // --------------------------------------------------------------------------
107
+ describe('Group B: Medusa Store Standard Endpoints', () => {
108
+ let provider: MedusaProvider;
109
+
110
+ beforeAll(() => {
111
+ provider = new MedusaProvider({
112
+ baseUrl,
113
+ publishableKey,
114
+ currencyCode: 'USD',
115
+ adminConfig: { apiKey: secretKey },
116
+ });
117
+ });
118
+
119
+ it('GET /store/products (list)', async () => {
120
+ const products = await provider.getProducts({ first: 3 });
121
+ expect(products).toBeInstanceOf(Array);
122
+ expect(products.length).toBeGreaterThan(0);
123
+ expect(products[0]).toHaveProperty('id');
124
+ expect(products[0]).toHaveProperty('title');
125
+ });
126
+
127
+ it('GET /store/products (by handle)', async () => {
128
+ const products = await provider.getProducts({ first: 1 });
129
+ expect(products.length).toBeGreaterThan(0);
130
+ const handle = products[0].handle;
131
+ expect(handle).toBeTruthy();
132
+
133
+ const product = await provider.getProduct(handle!);
134
+ expect(product).toBeDefined();
135
+ expect(product!.handle).toBe(handle);
136
+ });
137
+
138
+ it('GET /store/collections', async () => {
139
+ const collections = await provider.getCollections({});
140
+ expect(collections).toBeInstanceOf(Array);
141
+ });
142
+
143
+ it('GET /store/product-categories', async () => {
144
+ const categories = await provider.getCategories({ limit: 10 });
145
+ expect(categories).toBeInstanceOf(Array);
146
+ });
147
+
148
+ it('GET /store/regions', async () => {
149
+ const regions = await provider.getRegions();
150
+ expect(regions).toBeInstanceOf(Array);
151
+ expect(regions.length).toBeGreaterThan(0);
152
+ expect(regions[0]).toHaveProperty('id');
153
+ });
154
+
155
+ it('GET /store/regions/:id (countries)', async () => {
156
+ const regions = await provider.getRegions();
157
+ expect(regions.length).toBeGreaterThan(0);
158
+
159
+ const countries = await provider.getRegionCountries(regions[0].id);
160
+ expect(countries).toBeInstanceOf(Array);
161
+ });
162
+
163
+ describe('Cart lifecycle', () => {
164
+ let cartId: string;
165
+
166
+ it('POST /store/carts (create)', async () => {
167
+ const cart = await provider.createCart({})
168
+ expect(cart).toHaveProperty('id')
169
+ expect(cart.id).toBeTruthy()
170
+ cartId = cart.id as string
171
+ });
172
+
173
+ it('GET /store/carts/:id (retrieve)', async () => {
174
+ expect(cartId).toBeTruthy()
175
+ const cart = await provider.getCart(cartId as string)
176
+ expect(cart).toHaveProperty('id')
177
+ expect(cart!.id).toBe(cartId)
178
+ });
179
+
180
+ it('POST /store/carts/:id/line-items (add item)', async () => {
181
+ expect(cartId).toBeTruthy()
182
+ const products = await provider.getProducts({ first: 1 })
183
+ expect(products.length).toBeGreaterThan(0)
184
+ const variant = products[0].variants?.[0]
185
+ expect(variant).toBeDefined()
186
+
187
+ const cart = await provider.addToCart(cartId as string, [{
188
+ merchandiseId: variant!.id,
189
+ quantity: 1,
190
+ }])
191
+ expect(cart.lines.length).toBeGreaterThan(0)
192
+ });
193
+
194
+ it('POST /store/carts/:id/line-items/:lineId (update quantity)', async () => {
195
+ expect(cartId).toBeTruthy()
196
+ const cart = await provider.getCart(cartId as string)
197
+ expect(cart!.lines.length).toBeGreaterThan(0)
198
+ const lineId = cart!.lines[0].id as string
199
+
200
+ const updated = await provider.updateCart(cartId as string, [{
201
+ id: lineId,
202
+ merchandiseId: cart!.lines[0].merchandise.id,
203
+ quantity: 2,
204
+ }])
205
+ const line = updated.lines.find((l) => l.id === lineId)
206
+ expect(line?.quantity).toBe(2)
207
+ });
208
+
209
+ it('DELETE /store/carts/:id/line-items/:lineId (remove item)', async () => {
210
+ expect(cartId).toBeTruthy()
211
+ const cart = await provider.getCart(cartId as string)
212
+ expect(cart!.lines.length).toBeGreaterThan(0)
213
+ const lineId = cart!.lines[0].id as string
214
+
215
+ const updated = await provider.removeFromCart(cartId as string, [lineId])
216
+ expect(updated.lines.find((l) => l.id === lineId)).toBeUndefined()
217
+ });
218
+ });
219
+
220
+ it('GET /store/payment-providers', async () => {
221
+ const regions = await provider.getRegions()
222
+ expect(regions.length).toBeGreaterThan(0)
223
+
224
+ const methods = await provider.getPaymentMethods({ regionId: regions[0].id })
225
+ expect(methods).toBeInstanceOf(Array)
226
+ });
227
+ });
228
+
229
+ // --------------------------------------------------------------------------
230
+ // Group C: Customer Authentication (login, profile, addresses)
231
+ // --------------------------------------------------------------------------
232
+ describe('Group C: Customer Authentication', () => {
233
+ const testEmail = optionalEnv('MEDUSA_TEST_CUSTOMER_EMAIL')
234
+ const testPassword = optionalEnv('MEDUSA_TEST_CUSTOMER_PASSWORD')
235
+
236
+ it('login with test customer', async () => {
237
+ if (!testEmail || !testPassword) {
238
+ console.log('Skipping: MEDUSA_TEST_CUSTOMER_EMAIL/PASSWORD not set')
239
+ return
240
+ }
241
+
242
+ const provider = new MedusaProvider({
243
+ baseUrl,
244
+ publishableKey,
245
+ currencyCode: 'USD',
246
+ auth: {
247
+ method: 'jwt',
248
+ storage: 'memory',
249
+ },
250
+ });
251
+
252
+ const authProvider = provider.auth
253
+ expect(authProvider).toBeDefined()
254
+
255
+ const result = await authProvider!.login({ email: testEmail, password: testPassword })
256
+ expect(result.token || result.customer).toBeTruthy()
257
+ expect(result.customer).toBeDefined()
258
+ expect(result.customer!.email).toBe(testEmail)
259
+
260
+ // getCurrentCustomer
261
+ const customer = await authProvider!.getCurrentCustomer()
262
+ expect(customer).toBeDefined()
263
+ expect(customer!.email).toBe(testEmail)
264
+
265
+ // getAddresses
266
+ const addresses = await authProvider!.getAddresses()
267
+ expect(addresses).toBeInstanceOf(Array)
268
+
269
+ // Logout
270
+ await authProvider!.logout()
271
+ });
272
+ });
273
+
274
+ // --------------------------------------------------------------------------
275
+ // Group D: Admin Thor Plugin Endpoints (direct HTTP with JWT)
276
+ //
277
+ // NOTE: MedusaAdminClient uses x-medusa-access-token which is NOT supported
278
+ // by Medusa v2. These tests use direct fetch with Bearer JWT to verify the
279
+ // endpoints exist and respond correctly. This documents a real adapter bug.
280
+ // --------------------------------------------------------------------------
281
+ describe('Group D: Admin Thor Plugin Endpoints (direct JWT)', () => {
282
+ let jwt: string
283
+
284
+ beforeAll(async () => {
285
+ // Obtain admin JWT
286
+ const res = await fetch(`${baseUrl}/auth/user/emailpass`, {
287
+ method: 'POST',
288
+ headers: { 'Content-Type': 'application/json' },
289
+ body: JSON.stringify({ email: adminEmail, password: adminPassword }),
290
+ })
291
+ const data = await res.json() as { token?: string }
292
+ expect(data.token).toBeTruthy()
293
+ jwt = data.token as string
294
+ })
295
+
296
+ async function adminFetch(path: string): Promise<any> {
297
+ const res = await fetch(`${baseUrl}${path}`, {
298
+ headers: { Authorization: `Bearer ${jwt}` },
299
+ })
300
+ if (!res.ok) {
301
+ const text = await res.text()
302
+ throw new Error(`${res.status} ${path}: ${text}`)
303
+ }
304
+ return res.json()
305
+ }
306
+
307
+ it('GET /admin/thor/sales-channels/:id/customers', async () => {
308
+ const result = await adminFetch(`/admin/thor/sales-channels/${salesChannelId}/customers?limit=5`)
309
+ expect(result).toHaveProperty('customers')
310
+ expect(result.customers).toBeInstanceOf(Array)
311
+ })
312
+
313
+ it('GET /admin/thor/sales-channels/:id/api-keys', async () => {
314
+ const result = await adminFetch(`/admin/thor/sales-channels/${salesChannelId}/api-keys`)
315
+ expect(result).toHaveProperty('api_keys')
316
+ expect(result.api_keys).toBeInstanceOf(Array)
317
+ })
318
+
319
+ it('GET /admin/thor/sales-channels/:id/categories', async () => {
320
+ const result = await adminFetch(`/admin/thor/sales-channels/${salesChannelId}/categories?limit=10`)
321
+ expect(result).toHaveProperty('categories')
322
+ expect(result.categories).toBeInstanceOf(Array)
323
+ })
324
+
325
+ it('GET /admin/thor/storefront-config/:id (may 404)', async () => {
326
+ const res = await fetch(`${baseUrl}/admin/thor/storefront-config/${salesChannelId}`, {
327
+ headers: { Authorization: `Bearer ${jwt}` },
328
+ })
329
+ // 200 or 404 are both acceptable
330
+ expect([200, 404]).toContain(res.status)
331
+ })
332
+
333
+ it('GET /admin/thor/site-config/:id (may 404)', async () => {
334
+ const res = await fetch(`${baseUrl}/admin/thor/site-config/${salesChannelId}`, {
335
+ headers: { Authorization: `Bearer ${jwt}` },
336
+ })
337
+ expect([200, 404]).toContain(res.status)
338
+ })
339
+
340
+ it('GET /admin/thor/site-config/:id/history (may 404)', async () => {
341
+ const res = await fetch(`${baseUrl}/admin/thor/site-config/${salesChannelId}/history`, {
342
+ headers: { Authorization: `Bearer ${jwt}` },
343
+ })
344
+ expect([200, 404]).toContain(res.status)
345
+ })
346
+
347
+ it('BUG: MedusaAdminClient x-medusa-access-token returns 401', async () => {
348
+ // This documents the adapter bug: MedusaAdminClient sends x-medusa-access-token
349
+ // but Medusa v2 only accepts Authorization: Bearer for admin routes.
350
+ const res = await fetch(`${baseUrl}/admin/thor/sales-channels/${salesChannelId}/customers?limit=1`, {
351
+ headers: { 'x-medusa-access-token': jwt },
352
+ })
353
+ expect(res.status).toBe(401)
354
+ })
355
+ });
356
+
357
+ // --------------------------------------------------------------------------
358
+ // Group E: Admin Standard Medusa Endpoints (via SDK with Basic auth)
359
+ // --------------------------------------------------------------------------
360
+ describe('Group E: Admin Standard Medusa Endpoints', () => {
361
+ let provider: MedusaProvider;
362
+
363
+ beforeAll(() => {
364
+ provider = new MedusaProvider({
365
+ baseUrl,
366
+ publishableKey,
367
+ currencyCode: 'USD',
368
+ adminConfig: { apiKey: secretKey },
369
+ });
370
+ });
371
+
372
+ it('GET /admin/products (list)', async () => {
373
+ const result = await provider.getAdminProducts({ limit: 3 })
374
+ expect(result).toHaveProperty('products')
375
+ expect(result.products).toBeInstanceOf(Array)
376
+ expect(result.products.length).toBeGreaterThan(0)
377
+ })
378
+
379
+ it('GET /admin/products/:id (retrieve)', async () => {
380
+ const list = await provider.getAdminProducts({ limit: 1 })
381
+ expect(list.products.length).toBeGreaterThan(0)
382
+ const id = list.products[0].id
383
+
384
+ const product = await provider.getAdminProduct(id)
385
+ expect(product).toBeDefined()
386
+ expect(product!.id).toBe(id)
387
+ })
388
+
389
+ it('GET /admin/orders (list)', async () => {
390
+ const result = await provider.getAdminOrders({ limit: 3 })
391
+ expect(result).toHaveProperty('orders')
392
+ expect(result.orders).toBeInstanceOf(Array)
393
+ })
394
+
395
+ it('GET /admin/customers (list)', async () => {
396
+ const result = await provider.getAdminCustomers({ limit: 3 })
397
+ expect(result).toHaveProperty('customers')
398
+ expect(result.customers).toBeInstanceOf(Array)
399
+ })
400
+
401
+ it('GET /admin/regions (list)', async () => {
402
+ const result = await provider.getAdminRegions()
403
+ expect(result).toBeInstanceOf(Array)
404
+ expect(result.length).toBeGreaterThan(0)
405
+ })
406
+
407
+ it('GET /admin/stock-locations (list)', async () => {
408
+ const result = await provider.getStockLocations()
409
+ expect(result).toBeInstanceOf(Array)
410
+ })
411
+ });
412
+ }