@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.
Files changed (37) hide show
  1. package/PROVIDER_IMPLEMENTATION_ANALYSIS.md +659 -0
  2. package/dist/index.d.mts +160 -1
  3. package/dist/index.d.ts +160 -1
  4. package/dist/index.js +384 -2
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +384 -2
  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/providers/medusa/admin.test.ts +404 -0
  19. package/src/providers/medusa/admin.ts +442 -0
  20. package/src/providers/medusa/auth.test.ts +137 -0
  21. package/src/providers/medusa/auth.ts +96 -1
  22. package/src/providers/medusa/index.ts +21 -1
  23. package/src/providers/shopify/auth.ts +6 -0
  24. package/src/resilience/README.md +70 -48
  25. package/src/core/errors.js +0 -161
  26. package/src/core/fetch-utils.js +0 -240
  27. package/src/core/logger.js +0 -63
  28. package/src/metadata/channel-scope-validation.js +0 -136
  29. package/src/metadata/channel-scope.js +0 -174
  30. package/src/metadata/index.js +0 -31
  31. package/src/providers/medusa/auth.js +0 -601
  32. package/src/providers/medusa/index.js +0 -2485
  33. package/src/providers/medusa/payment.js +0 -265
  34. package/src/providers/medusa/transforms.js +0 -505
  35. package/src/types/capabilities.types.js +0 -141
  36. package/src/types/provider.js +0 -9
  37. package/src/utils/payment-utils.js +0 -152
@@ -49,6 +49,7 @@ 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';
52
53
  import {
53
54
  buildChannelScopeMetadata,
54
55
  filterByChannel,
@@ -163,6 +164,19 @@ export class MedusaProvider implements CommerceProvider {
163
164
  * Only created when adminConfig.apiKey is provided.
164
165
  */
165
166
  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;
166
180
  /** Cached distinct product status values — rarely changes, cached for process lifetime */
167
181
  private _cachedProductStatuses: string[] | null = null;
168
182
 
@@ -253,12 +267,18 @@ export class MedusaProvider implements CommerceProvider {
253
267
  debug: config.debug ?? false,
254
268
  // No publishableKey or auth config: this instance is admin-only, no JWT needed
255
269
  });
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
+ });
256
276
  this.logger.info('[Admin Auth] Dedicated admin SDK instance created with API key');
257
277
  }
258
278
 
259
279
  // Initialize auth provider if config is provided
260
280
  if (config.auth) {
261
- this.auth = new MedusaAuthProvider(this.sdk, config.auth, config.debug);
281
+ this.auth = new MedusaAuthProvider(this.sdk, config.auth, config.debug, config.baseUrl, config.publishableKey);
262
282
  this.logger.info('Auth provider initialized');
263
283
  }
264
284
 
@@ -399,4 +399,10 @@ export class ShopifyAuthProvider implements AuthProvider {
399
399
  async deleteCustomerAccount(): Promise<void> {
400
400
  throw new ProviderAPIError('Shopify customer self-service deletion requires Admin API and is not supported.', 'shopify', 501);
401
401
  }
402
+
403
+ async linkCustomerToChannel(): Promise<{ linked: boolean; reason?: string }> {
404
+ // Shopify does not have the concept of Thor sales channels.
405
+ // Return a no-op response so multi-tenant flows don't break when running against Shopify.
406
+ return { linked: false, reason: 'not_applicable' };
407
+ }
402
408
  }
@@ -1,3 +1,14 @@
1
+ ---
2
+ title: Resilience Patterns
3
+ purpose: Documents the Circuit Breaker and retry patterns for robust backend integration
4
+ type: reference
5
+ tags: [resilience, circuit-breaker, retry, fetch, patterns]
6
+ related:
7
+ - ../../docs/fetch-reliability-solution.md
8
+ - ../../docs/RETRY_SOLUTION_SUMMARY.md
9
+ updated: 2026-05-03
10
+ ---
11
+
1
12
  # Resilience Patterns
2
13
 
3
14
  **Framework-agnostic resilience patterns for robust backend integration.**
@@ -10,10 +21,17 @@ This module provides enterprise-grade resilience patterns for handling backend f
10
21
 
11
22
  ---
12
23
 
13
- ## 📦 Export
24
+ ## 📦 Exports
14
25
 
15
26
  ```typescript
16
- import { CircuitBreaker, backendCircuitBreaker } from '@thorprovider/adapters/resilience';
27
+ import {
28
+ CircuitBreaker,
29
+ backendCircuitBreaker,
30
+ CircuitBreakerOpenError,
31
+ type CircuitState,
32
+ type CircuitBreakerConfig,
33
+ type CircuitBreakerStats,
34
+ } from '@thorprovider/adapters';
17
35
  ```
18
36
 
19
37
  ---
@@ -53,7 +71,7 @@ The Circuit Breaker pattern prevents cascading failures when the backend is unav
53
71
  ### Basic Usage
54
72
 
55
73
  ```typescript
56
- import { CircuitBreaker } from '@thorprovider/adapters/resilience';
74
+ import { CircuitBreaker } from '@thorprovider/adapters';
57
75
 
58
76
  const breaker = new CircuitBreaker({
59
77
  failureThreshold: 5, // Open after 5 failures
@@ -63,7 +81,7 @@ const breaker = new CircuitBreaker({
63
81
 
64
82
  // With fallback
65
83
  const data = await breaker.execute(
66
- () => fetch('/api/data'),
84
+ () => fetch('/api/data').then(r => r.json()),
67
85
  { default: 'value' } // Fallback if circuit is open
68
86
  );
69
87
 
@@ -81,11 +99,11 @@ try {
81
99
  interface CircuitBreakerConfig {
82
100
  /** Number of failures before opening circuit */
83
101
  failureThreshold: number;
84
-
102
+
85
103
  /** Time in ms to wait before attempting recovery */
86
104
  resetTimeout: number;
87
-
88
- /** Name for logging/监控 */
105
+
106
+ /** Name for logging/monitoring */
89
107
  name?: string;
90
108
  }
91
109
  ```
@@ -103,7 +121,7 @@ console.log({
103
121
  failureCount: stats.failureCount,
104
122
  totalRequests: stats.totalRequests,
105
123
  totalFailures: stats.totalFailures,
106
- totalSuccesses: stats.totalSuccesses
124
+ totalSuccesses: stats.totalSuccesses,
107
125
  });
108
126
 
109
127
  // Manual reset (force recovery)
@@ -117,35 +135,34 @@ breaker.manualReset();
117
135
  A pre-configured global instance for backend API calls:
118
136
 
119
137
  ```typescript
120
- import { backendCircuitBreaker } from '@thorprovider/adapters/resilience';
138
+ import { backendCircuitBreaker } from '@thorprovider/adapters';
121
139
 
122
140
  // Use in adapters
123
141
  async function getProducts() {
124
142
  return backendCircuitBreaker.execute(
125
- () => fetch('/api/products'),
143
+ () => commerce.getProducts(),
126
144
  [] // Empty array fallback
127
145
  );
128
146
  }
129
147
  ```
130
148
 
131
- **Configuration:**
149
+ **Default configuration:**
132
150
  - `failureThreshold`: 5 failures
133
151
  - `resetTimeout`: 60,000ms (1 minute)
134
- - `name`: 'backend-api'
152
+ - `name`: `'backend-api'`
135
153
 
136
154
  ---
137
155
 
138
156
  ## 🎨 Usage Patterns
139
157
 
140
- ### Pattern 1: L2 Adapter with Graceful Degradation
158
+ ### Pattern 1: Adapter with Graceful Degradation
141
159
 
142
160
  ```typescript
143
- // packages/adapters/src/medusa/product-adapter.ts
144
- import { backendCircuitBreaker } from '@thorprovider/adapters/resilience';
161
+ import { backendCircuitBreaker } from '@thorprovider/adapters';
145
162
 
146
163
  export async function getProducts(options: GetProductsOptions) {
147
164
  return backendCircuitBreaker.execute(
148
- () => medusaClient.products.list(options),
165
+ () => commerce.getProducts(options),
149
166
  [] // Return empty array if backend is down
150
167
  );
151
168
  }
@@ -156,23 +173,23 @@ export async function getProducts(options: GetProductsOptions) {
156
173
  - ✅ User sees "No products" instead of error page
157
174
  - ✅ Backend gets time to recover
158
175
 
159
- ### Pattern 2: L5 Server Component with Fallback UI
176
+ ### Pattern 2: Server Component with Fallback UI
160
177
 
161
178
  ```typescript
162
- // packages/core/app/products/page.tsx
163
- import { backendCircuitBreaker } from '@thorprovider/adapters/resilience';
164
- import { getProducts } from '@thorprovider/adapters/medusa';
179
+ // app/products/page.tsx
180
+ import { backendCircuitBreaker } from '@thorprovider/adapters';
181
+ import { commerce } from '@/lib/commerce';
165
182
 
166
183
  export default async function ProductsPage() {
167
184
  const products = await backendCircuitBreaker.execute(
168
- () => getProducts(),
169
- [] // Fallback to empty array
185
+ () => commerce.getProducts(),
186
+ []
170
187
  );
171
-
188
+
172
189
  if (products.length === 0) {
173
- return <EmptyState message="Products temporarily unavailable" />;
190
+ return <p>Products temporarily unavailable</p>;
174
191
  }
175
-
192
+
176
193
  return <ProductsGrid products={products} />;
177
194
  }
178
195
  ```
@@ -181,14 +198,13 @@ export default async function ProductsPage() {
181
198
 
182
199
  ```typescript
183
200
  // Critical operations should fail explicitly
184
- async function completeCheckout(data: CheckoutData) {
201
+ async function completeCheckout(cartId: string) {
185
202
  try {
186
203
  return await backendCircuitBreaker.execute(
187
- () => medusaClient.carts.complete(data)
188
- // NO fallback - we want this to fail if backend is down
204
+ () => commerce.completeCheckout(cartId)
205
+ // NO fallback — we want this to fail if backend is down
189
206
  );
190
207
  } catch (error) {
191
- // Show error message to user
192
208
  throw new Error('Checkout unavailable. Please try again later.');
193
209
  }
194
210
  }
@@ -198,10 +214,9 @@ async function completeCheckout(data: CheckoutData) {
198
214
 
199
215
  ## 🧪 Testing
200
216
 
201
- ### Unit Test Example
202
-
203
217
  ```typescript
204
- import { CircuitBreaker } from '@thorprovider/adapters/resilience';
218
+ import { describe, it, expect, vi } from 'vitest';
219
+ import { CircuitBreaker } from '@thorprovider/adapters';
205
220
 
206
221
  describe('CircuitBreaker', () => {
207
222
  it('opens after threshold failures', async () => {
@@ -209,28 +224,28 @@ describe('CircuitBreaker', () => {
209
224
  failureThreshold: 3,
210
225
  resetTimeout: 1000,
211
226
  });
212
-
213
- const mockFn = jest.fn().mockRejectedValue(new Error('fail'));
214
-
227
+
228
+ const mockFn = vi.fn().mockRejectedValue(new Error('fail'));
229
+
215
230
  // 3 failures
216
231
  await expect(breaker.execute(mockFn)).rejects.toThrow();
217
232
  await expect(breaker.execute(mockFn)).rejects.toThrow();
218
233
  await expect(breaker.execute(mockFn)).rejects.toThrow();
219
-
234
+
220
235
  expect(breaker.state).toBe('OPEN');
221
236
  });
222
-
237
+
223
238
  it('uses fallback when circuit is open', async () => {
224
239
  const breaker = new CircuitBreaker({
225
240
  failureThreshold: 1,
226
241
  resetTimeout: 1000,
227
242
  });
228
-
229
- const mockFn = jest.fn().mockRejectedValue(new Error('fail'));
230
-
243
+
244
+ const mockFn = vi.fn().mockRejectedValue(new Error('fail'));
245
+
231
246
  // Open circuit
232
247
  await breaker.execute(mockFn, 'fallback').catch(() => {});
233
-
248
+
234
249
  // Should return fallback immediately
235
250
  const result = await breaker.execute(mockFn, 'fallback');
236
251
  expect(result).toBe('fallback');
@@ -239,6 +254,12 @@ describe('CircuitBreaker', () => {
239
254
  });
240
255
  ```
241
256
 
257
+ Run tests:
258
+
259
+ ```bash
260
+ yarn workspace @thorprovider/adapters test
261
+ ```
262
+
242
263
  ---
243
264
 
244
265
  ## 📐 Architecture Decisions
@@ -271,29 +292,30 @@ describe('CircuitBreaker', () => {
271
292
 
272
293
  - Don't use for client-side only operations
273
294
  - Don't ignore OPEN state errors silently
274
- - Don't set failureThreshold too low (causes flapping)
275
- - Don't set resetTimeout too high (slow recovery)
295
+ - Don't set `failureThreshold` too low (causes flapping)
296
+ - Don't set `resetTimeout` too high (slow recovery)
276
297
 
277
298
  ---
278
299
 
279
300
  ## 🔮 Future Enhancements
280
301
 
281
- - [ ] **Retry Pattern**: Exponential backoff for transient failures
282
- - [ ] **Bulkhead Pattern**: Isolate resource pools
302
+ - [ ] **Bulkhead Pattern**: Isolate resource pools per service
283
303
  - [ ] **Rate Limiting**: Prevent API abuse
284
304
  - [ ] **Metrics Export**: Prometheus/Datadog integration
285
305
  - [ ] **Health Check Integration**: Auto-open circuit on health check fail
286
306
 
307
+ > ✅ **Retry Pattern** with exponential backoff is already implemented in `src/core/fetch-utils.ts`.
308
+
287
309
  ---
288
310
 
289
311
  ## 📚 References
290
312
 
291
- - [Martin Fowler - Circuit Breaker](https://martinfowler.com/bliki/CircuitBreaker.html)
292
- - [Microsoft - Resilience Patterns](https://docs.microsoft.com/en-us/azure/architecture/patterns/circuit-breaker)
313
+ - [Martin Fowler — Circuit Breaker](https://martinfowler.com/bliki/CircuitBreaker.html)
314
+ - [Microsoft — Resilience Patterns](https://docs.microsoft.com/en-us/azure/architecture/patterns/circuit-breaker)
293
315
  - [Netflix Hystrix](https://github.com/Netflix/Hystrix/wiki)
294
316
 
295
317
  ---
296
318
 
297
- **Last Updated**: March 24, 2026
319
+ **Last Updated**: 2026-05-03
298
320
  **Status**: ✅ Active
299
321
  **Layer**: L2 (Adapters)
@@ -1,161 +0,0 @@
1
- "use strict";
2
- /**
3
- * @thorprovider/adapters v2.0
4
- * Core errors
5
- */
6
- var __extends = (this && this.__extends) || (function () {
7
- var extendStatics = function (d, b) {
8
- extendStatics = Object.setPrototypeOf ||
9
- ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
10
- function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
11
- return extendStatics(d, b);
12
- };
13
- return function (d, b) {
14
- if (typeof b !== "function" && b !== null)
15
- throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
16
- extendStatics(d, b);
17
- function __() { this.constructor = d; }
18
- d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
19
- };
20
- })();
21
- Object.defineProperty(exports, "__esModule", { value: true });
22
- exports.ProviderAPIError = exports.ResourceNotFoundError = exports.ConfigurationError = exports.ProviderNotFoundError = exports.CommerceError = void 0;
23
- exports.extractErrorMessage = extractErrorMessage;
24
- /**
25
- * Base error class for commerce providers
26
- */
27
- var CommerceError = /** @class */ (function (_super) {
28
- __extends(CommerceError, _super);
29
- function CommerceError(message, code, status) {
30
- var _this = _super.call(this, message) || this;
31
- _this.code = code;
32
- _this.status = status;
33
- _this.name = 'CommerceError';
34
- return _this;
35
- }
36
- return CommerceError;
37
- }(Error));
38
- exports.CommerceError = CommerceError;
39
- /**
40
- * Provider not found error
41
- */
42
- var ProviderNotFoundError = /** @class */ (function (_super) {
43
- __extends(ProviderNotFoundError, _super);
44
- function ProviderNotFoundError(providerName) {
45
- var _this = _super.call(this, "Provider not found: ".concat(providerName), 'PROVIDER_NOT_FOUND') || this;
46
- _this.name = 'ProviderNotFoundError';
47
- return _this;
48
- }
49
- return ProviderNotFoundError;
50
- }(CommerceError));
51
- exports.ProviderNotFoundError = ProviderNotFoundError;
52
- /**
53
- * Configuration error
54
- */
55
- var ConfigurationError = /** @class */ (function (_super) {
56
- __extends(ConfigurationError, _super);
57
- function ConfigurationError(message) {
58
- var _this = _super.call(this, message, 'CONFIGURATION_ERROR') || this;
59
- _this.name = 'ConfigurationError';
60
- return _this;
61
- }
62
- return ConfigurationError;
63
- }(CommerceError));
64
- exports.ConfigurationError = ConfigurationError;
65
- /**
66
- * Resource not found error
67
- */
68
- var ResourceNotFoundError = /** @class */ (function (_super) {
69
- __extends(ResourceNotFoundError, _super);
70
- function ResourceNotFoundError(resource, identifier) {
71
- var _this = _super.call(this, "".concat(resource, " not found: ").concat(identifier), 'RESOURCE_NOT_FOUND', 404) || this;
72
- _this.name = 'ResourceNotFoundError';
73
- return _this;
74
- }
75
- return ResourceNotFoundError;
76
- }(CommerceError));
77
- exports.ResourceNotFoundError = ResourceNotFoundError;
78
- /**
79
- * API error from provider
80
- */
81
- var ProviderAPIError = /** @class */ (function (_super) {
82
- __extends(ProviderAPIError, _super);
83
- function ProviderAPIError(message, provider, status) {
84
- var _this = _super.call(this, message, 'PROVIDER_API_ERROR', status) || this;
85
- _this.provider = provider;
86
- _this.name = 'ProviderAPIError';
87
- return _this;
88
- }
89
- return ProviderAPIError;
90
- }(CommerceError));
91
- exports.ProviderAPIError = ProviderAPIError;
92
- /**
93
- * Extract error message from various error types
94
- * Handles standard Error objects, API responses, and unknown error types
95
- */
96
- function extractErrorMessage(error) {
97
- var _a, _b, _c, _d, _e, _f;
98
- // Standard Error object
99
- if (error instanceof Error && error.message) {
100
- // Don't return generic messages if there's more data
101
- if (error.message !== 'An unknown error occurred.') {
102
- return error.message;
103
- }
104
- }
105
- // Object with message property
106
- if (typeof error === 'object' && error !== null) {
107
- var err = error;
108
- // Check for response body (common in fetch errors)
109
- if (err.body) {
110
- if (typeof err.body === 'string')
111
- return err.body;
112
- if (err.body.message)
113
- return String(err.body.message);
114
- if ((_a = err.body.error) === null || _a === void 0 ? void 0 : _a.message)
115
- return String(err.body.error.message);
116
- }
117
- // Check for response object
118
- if (err.response) {
119
- if ((_b = err.response.data) === null || _b === void 0 ? void 0 : _b.message)
120
- return String(err.response.data.message);
121
- if ((_c = err.response.data) === null || _c === void 0 ? void 0 : _c.error)
122
- return String(err.response.data.error);
123
- if (err.response.statusText)
124
- return "".concat(err.response.status || '', " ").concat(err.response.statusText).trim();
125
- }
126
- // Check for data property
127
- if ((_d = err.data) === null || _d === void 0 ? void 0 : _d.message)
128
- return String(err.data.message);
129
- if ((_e = err.data) === null || _e === void 0 ? void 0 : _e.error)
130
- return String(err.data.error);
131
- // Check for error property
132
- if ((_f = err.error) === null || _f === void 0 ? void 0 : _f.message)
133
- return String(err.error.message);
134
- // Check for statusText with status code
135
- if (err.status && err.statusText) {
136
- return "HTTP ".concat(err.status, ": ").concat(err.statusText);
137
- }
138
- if (err.status && !err.statusText) {
139
- return "HTTP ".concat(err.status, " Error");
140
- }
141
- // Check for message property (not generic)
142
- if (err.message && err.message !== 'An unknown error occurred.') {
143
- return String(err.message);
144
- }
145
- // Try to stringify the error object
146
- try {
147
- var stringified = JSON.stringify(error);
148
- if (stringified !== '{}')
149
- return stringified;
150
- }
151
- catch (_g) {
152
- // Ignore JSON.stringify errors
153
- }
154
- }
155
- // String error
156
- if (typeof error === 'string') {
157
- return error;
158
- }
159
- // Fallback
160
- return 'An unknown error occurred';
161
- }