@thorprovider/medusa-extended 1.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.
package/src/admin.ts ADDED
@@ -0,0 +1,441 @@
1
+ /**
2
+ * @fileoverview Medusa Multi-Tenant Admin Client
3
+ * @module @thorprovider/adapters/providers/medusa/admin
4
+ *
5
+ * Wrapper methods for all `/admin/thor/` endpoints defined in the Thor Commerce
6
+ * multi-tenant API contract. Requires an admin API key (`adminConfig.apiKey`)
7
+ * to be provided when creating the Medusa provider.
8
+ *
9
+ * Every method performs a raw `fetch` against the Medusa backend using the
10
+ * admin API key (`x-medusa-access-token`) — the Medusa JS-SDK's admin client
11
+ * does not expose these custom Thor plugin routes, so we use direct HTTP calls.
12
+ *
13
+ * Endpoint reference: `multitenant-api-contract.md`
14
+ */
15
+
16
+ import type {
17
+ GetChannelCustomersOptions,
18
+ GetChannelCustomersResponse,
19
+ GetChannelApiKeysResponse,
20
+ GetChannelCategoriesOptions,
21
+ GetChannelCategoriesResponse,
22
+ GetCategorySalesChannelsResponse,
23
+ AssignCategoriesToChannelsBody,
24
+ AssignCategoriesToChannelsResponse,
25
+ UnassignCategoriesFromChannelsBody,
26
+ UnassignCategoriesFromChannelsResponse,
27
+ GetCollectionSalesChannelsResponse,
28
+ AssignCollectionsToChannelsBody,
29
+ AssignCollectionsToChannelsResponse,
30
+ UnassignCollectionsFromChannelsBody,
31
+ UnassignCollectionsFromChannelsResponse,
32
+ GetAdminStorefrontConfigResponse,
33
+ UpdateStorefrontConfigBody,
34
+ UpdateStorefrontConfigResponse,
35
+ GetAdminSiteConfigResponse,
36
+ UpdateSiteConfigBody,
37
+ UpdateSiteConfigResponse,
38
+ GetAdminSiteConfigHistoryOptions,
39
+ GetAdminSiteConfigHistoryResponse,
40
+ RestoreSiteConfigResponse,
41
+ } from '@thorprovider/types';
42
+ import { createLogger, ProviderAPIError } from '@thorprovider/adapters';
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Internal helpers
46
+ // ---------------------------------------------------------------------------
47
+
48
+ type FetchMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
49
+
50
+ /**
51
+ * Configuration required to instantiate the admin client.
52
+ */
53
+ export interface MedusaAdminClientConfig {
54
+ /** Medusa backend base URL */
55
+ baseUrl: string;
56
+ /** Secret admin API key (x-medusa-access-token) */
57
+ apiKey: string;
58
+ /** Enable debug logging */
59
+ debug?: boolean;
60
+ }
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // MedusaAdminClient
64
+ // ---------------------------------------------------------------------------
65
+
66
+ /**
67
+ * HTTP client for Thor Commerce multi-tenant admin endpoints (`/admin/thor/`).
68
+ *
69
+ * All methods throw `ProviderAPIError` on non-2xx responses.
70
+ *
71
+ * @example
72
+ * ```typescript
73
+ * const admin = new MedusaAdminClient({
74
+ * baseUrl: process.env.MEDUSA_BACKEND_URL!,
75
+ * apiKey: process.env.MEDUSA_ADMIN_API_KEY!,
76
+ * });
77
+ *
78
+ * const { customers } = await admin.getChannelCustomers('sc_01JXXXXX');
79
+ * ```
80
+ */
81
+ export class MedusaAdminClient {
82
+ private baseUrl: string;
83
+ private apiKey: string;
84
+ private logger: ReturnType<typeof createLogger>;
85
+
86
+ constructor(config: MedusaAdminClientConfig) {
87
+ this.baseUrl = config.baseUrl.replace(/\/$/, '');
88
+ this.apiKey = config.apiKey;
89
+ this.logger = createLogger('MedusaAdminClient', config.debug);
90
+ }
91
+
92
+ // -----------------------------------------------------------------------
93
+ // Private — HTTP helper
94
+ // -----------------------------------------------------------------------
95
+
96
+ private async request<T>(
97
+ method: FetchMethod,
98
+ path: string,
99
+ body?: unknown,
100
+ ): Promise<T> {
101
+ const url = `${this.baseUrl}${path}`;
102
+ this.logger.debug(`[admin] ${method} ${path}`);
103
+
104
+ const headers: Record<string, string> = {
105
+ 'x-medusa-access-token': this.apiKey,
106
+ 'Content-Type': 'application/json',
107
+ };
108
+
109
+ const init: RequestInit = {
110
+ method,
111
+ headers,
112
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
113
+ };
114
+
115
+ let response: Response;
116
+ try {
117
+ response = await fetch(url, init);
118
+ } catch (networkError: any) {
119
+ throw new ProviderAPIError(
120
+ `[admin] Network error on ${method} ${path}: ${networkError.message}`,
121
+ 'NETWORK_ERROR',
122
+ networkError,
123
+ );
124
+ }
125
+
126
+ if (!response.ok) {
127
+ let message = `HTTP ${response.status}`;
128
+ try {
129
+ const json = await response.json();
130
+ message = json?.message ?? message;
131
+ } catch {
132
+ // ignore parse errors
133
+ }
134
+ throw new ProviderAPIError(
135
+ `[admin] ${method} ${path} failed: ${message}`,
136
+ `ADMIN_API_${response.status}`,
137
+ response.status,
138
+ );
139
+ }
140
+
141
+ return response.json() as Promise<T>;
142
+ }
143
+
144
+ // -----------------------------------------------------------------------
145
+ // 3.1 — Sales Channel: Customers
146
+ // -----------------------------------------------------------------------
147
+
148
+ /**
149
+ * Paginated list of customers linked to a sales channel.
150
+ *
151
+ * `GET /admin/thor/sales-channels/:id/customers`
152
+ */
153
+ async getChannelCustomers(
154
+ salesChannelId: string,
155
+ options: GetChannelCustomersOptions = {},
156
+ ): Promise<GetChannelCustomersResponse> {
157
+ const { limit = 20, offset = 0 } = options;
158
+ return this.request<GetChannelCustomersResponse>(
159
+ 'GET',
160
+ `/admin/thor/sales-channels/${salesChannelId}/customers?limit=${limit}&offset=${offset}`,
161
+ );
162
+ }
163
+
164
+ // -----------------------------------------------------------------------
165
+ // 3.2 — Sales Channel: API Keys
166
+ // -----------------------------------------------------------------------
167
+
168
+ /**
169
+ * Publishable API keys associated with a sales channel.
170
+ *
171
+ * `GET /admin/thor/sales-channels/:id/api-keys`
172
+ */
173
+ async getChannelApiKeys(salesChannelId: string): Promise<GetChannelApiKeysResponse> {
174
+ return this.request<GetChannelApiKeysResponse>(
175
+ 'GET',
176
+ `/admin/thor/sales-channels/${salesChannelId}/api-keys`,
177
+ );
178
+ }
179
+
180
+ // -----------------------------------------------------------------------
181
+ // 3.3 — Sales Channel: Categories
182
+ // -----------------------------------------------------------------------
183
+
184
+ /**
185
+ * Product categories assigned to a sales channel.
186
+ *
187
+ * `GET /admin/thor/sales-channels/:id/categories`
188
+ */
189
+ async getChannelCategories(
190
+ salesChannelId: string,
191
+ options: GetChannelCategoriesOptions = {},
192
+ ): Promise<GetChannelCategoriesResponse> {
193
+ const { limit = 20, offset = 0, include_descendants = false } = options;
194
+ const qs = new URLSearchParams({
195
+ limit: String(limit),
196
+ offset: String(offset),
197
+ include_descendants: String(include_descendants),
198
+ });
199
+ return this.request<GetChannelCategoriesResponse>(
200
+ 'GET',
201
+ `/admin/thor/sales-channels/${salesChannelId}/categories?${qs}`,
202
+ );
203
+ }
204
+
205
+ // -----------------------------------------------------------------------
206
+ // 3.4 — Category: Sales Channels
207
+ // -----------------------------------------------------------------------
208
+
209
+ /**
210
+ * Sales channels to which a category is assigned.
211
+ *
212
+ * `GET /admin/thor/categories/:id/sales-channels`
213
+ */
214
+ async getCategorySalesChannels(
215
+ categoryId: string,
216
+ ): Promise<GetCategorySalesChannelsResponse> {
217
+ return this.request<GetCategorySalesChannelsResponse>(
218
+ 'GET',
219
+ `/admin/thor/categories/${categoryId}/sales-channels`,
220
+ );
221
+ }
222
+
223
+ // -----------------------------------------------------------------------
224
+ // 3.5 — Category: Assign to Sales Channels
225
+ // -----------------------------------------------------------------------
226
+
227
+ /**
228
+ * Assign a category to one or more sales channels (idempotent).
229
+ *
230
+ * `POST /admin/thor/categories/:id/sales-channels`
231
+ */
232
+ async assignCategoryToChannels(
233
+ categoryId: string,
234
+ body: AssignCategoriesToChannelsBody,
235
+ ): Promise<AssignCategoriesToChannelsResponse> {
236
+ return this.request<AssignCategoriesToChannelsResponse>(
237
+ 'POST',
238
+ `/admin/thor/categories/${categoryId}/sales-channels`,
239
+ body,
240
+ );
241
+ }
242
+
243
+ // -----------------------------------------------------------------------
244
+ // 3.6 — Category: Unassign from Sales Channels
245
+ // -----------------------------------------------------------------------
246
+
247
+ /**
248
+ * Remove category assignment from one or more sales channels (idempotent).
249
+ *
250
+ * `DELETE /admin/thor/categories/:id/sales-channels`
251
+ */
252
+ async unassignCategoryFromChannels(
253
+ categoryId: string,
254
+ body: UnassignCategoriesFromChannelsBody,
255
+ ): Promise<UnassignCategoriesFromChannelsResponse> {
256
+ return this.request<UnassignCategoriesFromChannelsResponse>(
257
+ 'DELETE',
258
+ `/admin/thor/categories/${categoryId}/sales-channels`,
259
+ body,
260
+ );
261
+ }
262
+
263
+ // -----------------------------------------------------------------------
264
+ // 3.7 — Collection: Sales Channels
265
+ // -----------------------------------------------------------------------
266
+
267
+ /**
268
+ * Sales channels to which a collection is assigned.
269
+ *
270
+ * `GET /admin/thor/collections/:id/sales-channels`
271
+ */
272
+ async getCollectionSalesChannels(
273
+ collectionId: string,
274
+ ): Promise<GetCollectionSalesChannelsResponse> {
275
+ return this.request<GetCollectionSalesChannelsResponse>(
276
+ 'GET',
277
+ `/admin/thor/collections/${collectionId}/sales-channels`,
278
+ );
279
+ }
280
+
281
+ // -----------------------------------------------------------------------
282
+ // 3.8 — Collection: Assign to Sales Channels
283
+ // -----------------------------------------------------------------------
284
+
285
+ /**
286
+ * Assign a collection to one or more sales channels (idempotent).
287
+ *
288
+ * `POST /admin/thor/collections/:id/sales-channels`
289
+ */
290
+ async assignCollectionToChannels(
291
+ collectionId: string,
292
+ body: AssignCollectionsToChannelsBody,
293
+ ): Promise<AssignCollectionsToChannelsResponse> {
294
+ return this.request<AssignCollectionsToChannelsResponse>(
295
+ 'POST',
296
+ `/admin/thor/collections/${collectionId}/sales-channels`,
297
+ body,
298
+ );
299
+ }
300
+
301
+ // -----------------------------------------------------------------------
302
+ // 3.9 — Collection: Unassign from Sales Channels
303
+ // -----------------------------------------------------------------------
304
+
305
+ /**
306
+ * Remove collection assignment from one or more sales channels (idempotent).
307
+ *
308
+ * `DELETE /admin/thor/collections/:id/sales-channels`
309
+ */
310
+ async unassignCollectionFromChannels(
311
+ collectionId: string,
312
+ body: UnassignCollectionsFromChannelsBody,
313
+ ): Promise<UnassignCollectionsFromChannelsResponse> {
314
+ return this.request<UnassignCollectionsFromChannelsResponse>(
315
+ 'DELETE',
316
+ `/admin/thor/collections/${collectionId}/sales-channels`,
317
+ body,
318
+ );
319
+ }
320
+
321
+ // -----------------------------------------------------------------------
322
+ // 3.10 — Storefront Config: Read
323
+ // -----------------------------------------------------------------------
324
+
325
+ /**
326
+ * Read the visual storefront configuration for a channel.
327
+ * `:id` is the `sales_channel_id`.
328
+ *
329
+ * `GET /admin/thor/storefront-config/:id`
330
+ */
331
+ async getStorefrontConfig(
332
+ salesChannelId: string,
333
+ ): Promise<GetAdminStorefrontConfigResponse> {
334
+ return this.request<GetAdminStorefrontConfigResponse>(
335
+ 'GET',
336
+ `/admin/thor/storefront-config/${salesChannelId}`,
337
+ );
338
+ }
339
+
340
+ // -----------------------------------------------------------------------
341
+ // 3.11 — Storefront Config: Update
342
+ // -----------------------------------------------------------------------
343
+
344
+ /**
345
+ * Update the visual storefront configuration for a channel.
346
+ * Triggers the `storefront_config.updated` event → ISR webhook.
347
+ *
348
+ * `PUT /admin/thor/storefront-config/:id`
349
+ */
350
+ async updateStorefrontConfig(
351
+ salesChannelId: string,
352
+ body: UpdateStorefrontConfigBody,
353
+ ): Promise<UpdateStorefrontConfigResponse> {
354
+ return this.request<UpdateStorefrontConfigResponse>(
355
+ 'PUT',
356
+ `/admin/thor/storefront-config/${salesChannelId}`,
357
+ body,
358
+ );
359
+ }
360
+
361
+ // -----------------------------------------------------------------------
362
+ // 3.12 — Site Designer Config: Read
363
+ // -----------------------------------------------------------------------
364
+
365
+ /**
366
+ * Read the active Site Designer configuration for a channel, including the
367
+ * last 10 history entries.
368
+ *
369
+ * `GET /admin/thor/site-config/:sales_channel_id`
370
+ */
371
+ async getSiteConfig(salesChannelId: string): Promise<GetAdminSiteConfigResponse> {
372
+ return this.request<GetAdminSiteConfigResponse>(
373
+ 'GET',
374
+ `/admin/thor/site-config/${salesChannelId}`,
375
+ );
376
+ }
377
+
378
+ // -----------------------------------------------------------------------
379
+ // 3.13 — Site Designer Config: Update
380
+ // -----------------------------------------------------------------------
381
+
382
+ /**
383
+ * Update the Site Designer configuration for a channel.
384
+ * Creates a new history entry on every PUT.
385
+ *
386
+ * `PUT /admin/thor/site-config/:sales_channel_id`
387
+ */
388
+ async updateSiteConfig(
389
+ salesChannelId: string,
390
+ body: UpdateSiteConfigBody,
391
+ ): Promise<UpdateSiteConfigResponse> {
392
+ return this.request<UpdateSiteConfigResponse>(
393
+ 'PUT',
394
+ `/admin/thor/site-config/${salesChannelId}`,
395
+ body,
396
+ );
397
+ }
398
+
399
+ // -----------------------------------------------------------------------
400
+ // 3.14 — Site Designer Config: History
401
+ // -----------------------------------------------------------------------
402
+
403
+ /**
404
+ * Full paginated history of Site Designer versions for a channel.
405
+ * Entries are immutable — they are never deleted.
406
+ *
407
+ * `GET /admin/thor/site-config/:sales_channel_id/history`
408
+ */
409
+ async getSiteConfigHistory(
410
+ salesChannelId: string,
411
+ options: GetAdminSiteConfigHistoryOptions = {},
412
+ ): Promise<GetAdminSiteConfigHistoryResponse> {
413
+ const { limit = 20, offset = 0 } = options;
414
+ return this.request<GetAdminSiteConfigHistoryResponse>(
415
+ 'GET',
416
+ `/admin/thor/site-config/${salesChannelId}/history?limit=${limit}&offset=${offset}`,
417
+ );
418
+ }
419
+
420
+ // -----------------------------------------------------------------------
421
+ // 3.15 — Site Designer Config: Restore
422
+ // -----------------------------------------------------------------------
423
+
424
+ /**
425
+ * Restore a previous Site Designer version.
426
+ * Internally runs the update workflow with the stored config of the target
427
+ * version — this creates a **new** history entry and does NOT mutate past
428
+ * entries.
429
+ *
430
+ * `POST /admin/thor/site-config/:sales_channel_id/restore/:version`
431
+ */
432
+ async restoreSiteConfig(
433
+ salesChannelId: string,
434
+ version: string,
435
+ ): Promise<RestoreSiteConfigResponse> {
436
+ return this.request<RestoreSiteConfigResponse>(
437
+ 'POST',
438
+ `/admin/thor/site-config/${salesChannelId}/restore/${encodeURIComponent(version)}`,
439
+ );
440
+ }
441
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @thorprovider/medusa-extended
3
+ * Thor Commerce multi-tenant admin extensions for Medusa
4
+ */
5
+
6
+ export { MedusaAdminClient } from './admin';
7
+ export type { MedusaAdminClientConfig } from './admin';
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "./src",
5
+ "outDir": "dist",
6
+ "declaration": true,
7
+ "declarationMap": true,
8
+ "incremental": true,
9
+ "tsBuildInfoFile": "./dist/.tsbuildinfo",
10
+ "moduleResolution": "bundler",
11
+ "baseUrl": "."
12
+ },
13
+ "include": ["src/**/*"],
14
+ "exclude": ["node_modules", "dist", "**/*.test.ts"]
15
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from 'tsup'
2
+
3
+ export default defineConfig({
4
+ entry: {
5
+ 'index': 'src/index.ts'
6
+ },
7
+ format: ['cjs', 'esm'],
8
+ dts: true,
9
+ splitting: false,
10
+ sourcemap: true,
11
+ clean: true,
12
+ onSuccess: process.env.SKIP_TYPE_CHECK === 'true' ? undefined : 'tsc --noEmit --skipLibCheck'
13
+ })