@thorprovider/adapters 4.2.0 → 4.2.2

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 (48) hide show
  1. package/.turbo/turbo-build.log +18 -18
  2. package/.turbo/turbo-test.log +1 -0
  3. package/.turbo/turbo-type-check.log +0 -0
  4. package/AGENTS.md +1 -1
  5. package/CHANGELOG.md +12 -0
  6. package/PROVIDER_IMPLEMENTATION_ANALYSIS.md +659 -0
  7. package/README.md +22 -3
  8. package/dist/.tsbuildinfo +1 -1
  9. package/dist/index.d.mts +140 -2
  10. package/dist/index.d.ts +140 -2
  11. package/dist/index.js +1075 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/index.mjs +1070 -0
  14. package/dist/index.mjs.map +1 -1
  15. package/docs/RETRY_SOLUTION_SUMMARY.md +26 -11
  16. package/docs/authentication.md +89 -62
  17. package/docs/capabilities-system.md +100 -38
  18. package/docs/category-customization.md +37 -29
  19. package/docs/fetch-reliability-solution.md +127 -278
  20. package/docs/getting-started.md +43 -29
  21. package/docs/profile-management.md +68 -38
  22. package/docs/providers-overview.md +14 -0
  23. package/docs/services-overview.md +20 -2
  24. package/docs/update-documentation.md +10 -8
  25. package/package.json +11 -6
  26. package/publish.log +93 -0
  27. package/quick-reference.md +1 -0
  28. package/src/factory/index.ts +4 -0
  29. package/src/index.ts +7 -0
  30. package/src/providers/index.ts +3 -0
  31. package/src/providers/medusa/admin.test.ts +404 -0
  32. package/src/providers/medusa/admin.ts +442 -0
  33. package/src/providers/medusa/auth.test.ts +95 -0
  34. package/src/providers/medusa/index.test.ts +392 -0
  35. package/src/providers/medusa/live-smoke.test.ts +412 -0
  36. package/src/providers/stelorder/client.test-helpers.ts +3 -0
  37. package/src/providers/stelorder/client.test.ts +58 -0
  38. package/src/providers/stelorder/client.ts +326 -0
  39. package/src/providers/stelorder/index.test.ts +48 -0
  40. package/src/providers/stelorder/index.ts +685 -0
  41. package/src/providers/stelorder/live-smoke.test.ts +54 -0
  42. package/src/providers/stelorder/transforms.test.ts +103 -0
  43. package/src/providers/stelorder/transforms.ts +631 -0
  44. package/src/resilience/README.md +70 -48
  45. package/src/types/capabilities.types.ts +51 -0
  46. package/vitest.config.ts +13 -0
  47. package/vitest.live.config.ts +18 -0
  48. package/vitest.setup.ts +6 -0
@@ -0,0 +1,326 @@
1
+ import type { ProviderConfig } from '../../types/provider';
2
+ import { ConfigurationError, ProviderAPIError, extractErrorMessage } from '../../core/errors';
3
+ import { fetchWithRetry, type RetryConfig } from '../../core/fetch-utils';
4
+ import { createLogger, type Logger } from '../../core/logger';
5
+ import { normalizeStelCollection, normalizeStelEntity } from './transforms';
6
+
7
+ type FetchImplementation = (url: string, init?: RequestInit) => Promise<Response>;
8
+ type HttpMethod = 'DELETE' | 'GET' | 'POST' | 'PUT';
9
+
10
+ export type StelQueryValue =
11
+ | string
12
+ | number
13
+ | boolean
14
+ | null
15
+ | undefined
16
+ | Array<string | number | boolean>;
17
+
18
+ export type StelQueryParams = Record<string, StelQueryValue>;
19
+
20
+ export interface StelOrderProviderConfig extends ProviderConfig {
21
+ baseUrl: string;
22
+ apiKey?: string;
23
+ publishableKey?: string;
24
+ currencyCode?: string;
25
+ companyName?: string;
26
+ storefrontId?: string;
27
+ retry?: RetryConfig;
28
+ }
29
+
30
+ type RequiredRetryConfig = Required<RetryConfig>;
31
+
32
+ const DEFAULT_RETRY_CONFIG: RequiredRetryConfig = {
33
+ maxRetries: 2,
34
+ initialDelayMs: 300,
35
+ backoffMultiplier: 2,
36
+ maxDelayMs: 5000,
37
+ timeoutMs: 10000,
38
+ retryableStatusCodes: [408, 429, 500, 502, 503, 504],
39
+ };
40
+
41
+ function sleep(ms: number): Promise<void> {
42
+ return new Promise((resolve) => setTimeout(resolve, ms));
43
+ }
44
+
45
+ function calculateDelay(attempt: number, retryConfig: RequiredRetryConfig): number {
46
+ const delay = retryConfig.initialDelayMs * Math.pow(retryConfig.backoffMultiplier, attempt);
47
+ return Math.min(delay, retryConfig.maxDelayMs);
48
+ }
49
+
50
+ function isRetryableError(error: unknown, retryConfig: RequiredRetryConfig): boolean {
51
+ if (error instanceof ProviderAPIError) {
52
+ return typeof error.status === 'number' && retryConfig.retryableStatusCodes.includes(error.status);
53
+ }
54
+
55
+ if (!(error instanceof Error)) {
56
+ return false;
57
+ }
58
+
59
+ const message = error.message.toLowerCase();
60
+ return (
61
+ error.name === 'AbortError' ||
62
+ error.name === 'TimeoutError' ||
63
+ message.includes('fetch failed') ||
64
+ message.includes('timeout') ||
65
+ message.includes('econnrefused') ||
66
+ message.includes('etimedout')
67
+ );
68
+ }
69
+
70
+ function createRetryingFetch(
71
+ fetchImplementation: FetchImplementation,
72
+ retryConfig: RequiredRetryConfig,
73
+ logger: Logger,
74
+ ): FetchImplementation {
75
+ return async (url: string, init?: RequestInit): Promise<Response> => {
76
+ let lastError: unknown = new Error('Unknown STEL Order request error');
77
+
78
+ for (let attempt = 0; attempt <= retryConfig.maxRetries; attempt += 1) {
79
+ const controller = new AbortController();
80
+ const timeoutId = setTimeout(() => controller.abort(), retryConfig.timeoutMs);
81
+
82
+ try {
83
+ const response = await fetchImplementation(url, {
84
+ ...init,
85
+ signal: controller.signal,
86
+ });
87
+
88
+ clearTimeout(timeoutId);
89
+
90
+ if (!response.ok && retryConfig.retryableStatusCodes.includes(response.status)) {
91
+ throw new ProviderAPIError(
92
+ `STEL Order API error: ${response.status} ${response.statusText}`,
93
+ 'stelorder',
94
+ response.status,
95
+ );
96
+ }
97
+
98
+ return response;
99
+ } catch (error) {
100
+ clearTimeout(timeoutId);
101
+ lastError = error;
102
+
103
+ if (attempt >= retryConfig.maxRetries || !isRetryableError(error, retryConfig)) {
104
+ throw error;
105
+ }
106
+
107
+ const delay = calculateDelay(attempt, retryConfig);
108
+ logger.warn('STEL request failed, retrying.', {
109
+ url,
110
+ attempt: attempt + 1,
111
+ maxAttempts: retryConfig.maxRetries + 1,
112
+ nextRetryIn: delay,
113
+ error: extractErrorMessage(error),
114
+ });
115
+
116
+ await sleep(delay);
117
+ }
118
+ }
119
+
120
+ throw lastError;
121
+ };
122
+ }
123
+
124
+ async function parseErrorResponse(response: Response): Promise<string> {
125
+ const contentType = response.headers.get('content-type') ?? '';
126
+
127
+ try {
128
+ if (contentType.includes('application/json')) {
129
+ const body = (await response.json()) as unknown;
130
+ const message = extractErrorMessage(body);
131
+ if (message && message !== 'An unknown error occurred') {
132
+ return message;
133
+ }
134
+ }
135
+ } catch {
136
+ // Ignore body parsing issues and fall back to text/status.
137
+ }
138
+
139
+ try {
140
+ const text = await response.text();
141
+ if (text.trim()) {
142
+ return text.trim();
143
+ }
144
+ } catch {
145
+ // Ignore body parsing issues and fall back to status.
146
+ }
147
+
148
+ return `STEL Order API error: ${response.status} ${response.statusText}`;
149
+ }
150
+
151
+ export function normalizeStelApiBaseUrl(baseUrl: string): string {
152
+ const trimmed = baseUrl.trim();
153
+
154
+ if (!trimmed) {
155
+ throw new ConfigurationError('Stel Order baseUrl is required.');
156
+ }
157
+
158
+ const normalized = trimmed
159
+ .replace(/\/api\/openapi\.json$/i, '')
160
+ .replace(/\/+$/, '');
161
+
162
+ return normalized.endsWith('/app') ? normalized : `${normalized}/app`;
163
+ }
164
+
165
+ interface StelRequestOptions {
166
+ method: HttpMethod;
167
+ path: string;
168
+ query?: StelQueryParams;
169
+ body?: unknown;
170
+ headers?: HeadersInit;
171
+ }
172
+
173
+ export class StelOrderClient {
174
+ readonly apiBaseUrl: string;
175
+
176
+ private readonly apiKey: string;
177
+ private readonly logger: Logger;
178
+ private readonly retryConfig: RequiredRetryConfig;
179
+ private readonly fetchImplementation?: FetchImplementation;
180
+
181
+ constructor(config: StelOrderProviderConfig) {
182
+ const apiKey = config.apiKey ?? config.publishableKey;
183
+
184
+ if (!apiKey) {
185
+ throw new ConfigurationError(
186
+ 'Stel Order API key is required. Pass `apiKey` or `publishableKey` to createProvider().',
187
+ );
188
+ }
189
+
190
+ this.apiBaseUrl = normalizeStelApiBaseUrl(config.baseUrl);
191
+ this.apiKey = apiKey;
192
+ this.logger = createLogger('StelOrderClient', config.debug);
193
+ this.retryConfig = {
194
+ ...DEFAULT_RETRY_CONFIG,
195
+ ...config.retry,
196
+ };
197
+ this.fetchImplementation = config.customFetch
198
+ ? createRetryingFetch(config.customFetch, this.retryConfig, this.logger)
199
+ : undefined;
200
+
201
+ this.logger.info('Initializing Stel Order client', {
202
+ apiBaseUrl: this.apiBaseUrl,
203
+ retryConfig: this.retryConfig,
204
+ });
205
+ }
206
+
207
+ async list<T>(resource: string, query?: StelQueryParams): Promise<T[]> {
208
+ const payload = await this.request<T | T[]>({
209
+ method: 'GET',
210
+ path: resource,
211
+ query,
212
+ });
213
+
214
+ return normalizeStelCollection(payload);
215
+ }
216
+
217
+ async getOne<T>(resource: string, id: string | number, query?: StelQueryParams): Promise<T | undefined> {
218
+ const payload = await this.request<T | T[]>({
219
+ method: 'GET',
220
+ path: `${resource}/${id}`,
221
+ query,
222
+ });
223
+
224
+ return normalizeStelEntity(payload);
225
+ }
226
+
227
+ async create<TResponse, TBody = unknown>(resource: string, body: TBody): Promise<TResponse> {
228
+ return this.request<TResponse>({
229
+ method: 'POST',
230
+ path: resource,
231
+ body,
232
+ });
233
+ }
234
+
235
+ async update<TResponse, TBody = unknown>(resource: string, id: string | number, body: TBody): Promise<TResponse> {
236
+ return this.request<TResponse>({
237
+ method: 'PUT',
238
+ path: `${resource}/${id}`,
239
+ body,
240
+ });
241
+ }
242
+
243
+ async remove(resource: string, id: string | number): Promise<void> {
244
+ await this.request<void>({
245
+ method: 'DELETE',
246
+ path: `${resource}/${id}`,
247
+ });
248
+ }
249
+
250
+ private buildUrl(path: string, query?: StelQueryParams): string {
251
+ const normalizedPath = path.replace(/^\/+/, '');
252
+ const url = new URL(`${this.apiBaseUrl}/${normalizedPath}`);
253
+
254
+ if (!query) {
255
+ return url.toString();
256
+ }
257
+
258
+ for (const [key, value] of Object.entries(query)) {
259
+ if (value === undefined || value === null || value === '') {
260
+ continue;
261
+ }
262
+
263
+ if (Array.isArray(value)) {
264
+ url.searchParams.set(key, value.map(String).join(','));
265
+ continue;
266
+ }
267
+
268
+ url.searchParams.set(key, String(value));
269
+ }
270
+
271
+ return url.toString();
272
+ }
273
+
274
+ private buildHeaders(headers?: HeadersInit, hasBody: boolean = false): Headers {
275
+ const normalizedHeaders = new Headers(headers);
276
+
277
+ normalizedHeaders.set('Accept', 'application/json');
278
+ normalizedHeaders.set('APIKEY', this.apiKey);
279
+
280
+ if (hasBody && !normalizedHeaders.has('Content-Type')) {
281
+ normalizedHeaders.set('Content-Type', 'application/json');
282
+ }
283
+
284
+ return normalizedHeaders;
285
+ }
286
+
287
+ private async executeFetch(url: string, init: RequestInit): Promise<Response> {
288
+ if (this.fetchImplementation) {
289
+ return this.fetchImplementation(url, init);
290
+ }
291
+
292
+ return fetchWithRetry(url, init, this.retryConfig);
293
+ }
294
+
295
+ private async request<TResponse>(options: StelRequestOptions): Promise<TResponse> {
296
+ const url = this.buildUrl(options.path, options.query);
297
+ const hasBody = options.body !== undefined;
298
+
299
+ this.logger.debug('STEL request', {
300
+ method: options.method,
301
+ url,
302
+ });
303
+
304
+ const response = await this.executeFetch(url, {
305
+ method: options.method,
306
+ headers: this.buildHeaders(options.headers, hasBody),
307
+ body: hasBody ? JSON.stringify(options.body) : undefined,
308
+ });
309
+
310
+ if (!response.ok) {
311
+ const message = await parseErrorResponse(response);
312
+ throw new ProviderAPIError(message, 'stelorder', response.status);
313
+ }
314
+
315
+ if (response.status === 204) {
316
+ return undefined as TResponse;
317
+ }
318
+
319
+ const contentType = response.headers.get('content-type') ?? '';
320
+ if (contentType.includes('application/json')) {
321
+ return (await response.json()) as TResponse;
322
+ }
323
+
324
+ return (await response.text()) as TResponse;
325
+ }
326
+ }
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { http, HttpResponse } from 'msw'
3
+ import { server } from './client.test-helpers'
4
+ import { StelOrderProvider } from './index'
5
+
6
+ describe('StelOrderProvider', () => {
7
+ it('returns storefront context and supports read methods', async () => {
8
+ server.use(
9
+ http.get('https://stel.example.com/app/products', () =>
10
+ HttpResponse.json([{ id: 10, name: 'Test Product', 'full-reference': 'TP-10', 'sales-price': 10 }]),
11
+ ),
12
+ http.get('https://stel.example.com/app/productCategories', () =>
13
+ HttpResponse.json([{ id: 2, name: 'Hardware', slug: 'hardware' }]),
14
+ ),
15
+ )
16
+
17
+ const provider = new StelOrderProvider({
18
+ baseUrl: 'https://stel.example.com',
19
+ apiKey: 'secret',
20
+ companyName: 'Test Co',
21
+ storefrontId: 'stel-1',
22
+ })
23
+
24
+ const context = await provider.getStorefrontContext()
25
+ expect(context.platformType).toBe('stelorder')
26
+ expect(context.name).toBe('Test Co')
27
+
28
+ await expect(provider.getProducts()).resolves.toHaveLength(1)
29
+ await expect(provider.getCategories()).resolves.toHaveLength(1)
30
+ })
31
+
32
+ it('throws clear errors for unsupported workflows', async () => {
33
+ const provider = new StelOrderProvider({
34
+ baseUrl: 'https://stel.example.com',
35
+ apiKey: 'secret',
36
+ })
37
+
38
+ await expect(provider.createCart()).rejects.toMatchObject({
39
+ name: 'ProviderAPIError',
40
+ status: 501,
41
+ })
42
+
43
+ await expect(provider.completeCheckout('cart-1')).rejects.toMatchObject({
44
+ name: 'ProviderAPIError',
45
+ status: 501,
46
+ })
47
+ })
48
+ })