@thorprovider/adapters 3.0.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.
- package/.turbo/turbo-build.log +18 -0
- package/.turbo/turbo-test.log +1 -0
- package/.turbo/turbo-type-check.log +0 -0
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +271 -0
- package/README.md +634 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/index.d.mts +2524 -0
- package/dist/index.d.ts +2524 -0
- package/dist/index.js +8136 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +8045 -0
- package/dist/index.mjs.map +1 -0
- package/docs/RETRY_SOLUTION_SUMMARY.md +55 -0
- package/docs/authentication.md +165 -0
- package/docs/capabilities-system.md +143 -0
- package/docs/category-customization.md +309 -0
- package/docs/fetch-reliability-solution.md +399 -0
- package/docs/getting-started.md +154 -0
- package/docs/multi-tenant-channel-scoping.md +225 -0
- package/docs/profile-management.md +147 -0
- package/docs/providers-overview.md +85 -0
- package/docs/services-overview.md +135 -0
- package/docs/update-documentation.md +113 -0
- package/package.json +43 -0
- package/publish.log +93 -0
- package/quick-reference.md +169 -0
- package/src/admin/AdminAuthService.ts +88 -0
- package/src/admin/AdminSessionService.ts +91 -0
- package/src/admin/MetricsService.ts +145 -0
- package/src/admin/index.ts +13 -0
- package/src/backend/BackendMetadataAdapter.ts +88 -0
- package/src/backend/factory.ts +107 -0
- package/src/backend/index.ts +12 -0
- package/src/backend/medusa/MedusaMetadataAdapter.ts +139 -0
- package/src/core/errors.js +161 -0
- package/src/core/errors.ts +132 -0
- package/src/core/fetch-utils.js +240 -0
- package/src/core/fetch-utils.ts +227 -0
- package/src/core/index.ts +8 -0
- package/src/core/logger.js +63 -0
- package/src/core/logger.ts +37 -0
- package/src/factory/index.ts +102 -0
- package/src/index.ts +205 -0
- package/src/lib/money-transform.ts +80 -0
- package/src/metadata/channel-scope-validation.js +136 -0
- package/src/metadata/channel-scope-validation.ts +151 -0
- package/src/metadata/channel-scope.js +174 -0
- package/src/metadata/channel-scope.ts +215 -0
- package/src/metadata/index.js +31 -0
- package/src/metadata/index.ts +37 -0
- package/src/providers/bigcommerce/bigcommerce.ts +6 -0
- package/src/providers/bigcommerce/index.ts +315 -0
- package/src/providers/bigcommerce/transforms.ts +227 -0
- package/src/providers/index.ts +22 -0
- package/src/providers/magento/index.ts +387 -0
- package/src/providers/magento/magento.ts +5 -0
- package/src/providers/magento/queries.ts +267 -0
- package/src/providers/magento/transforms.ts +249 -0
- package/src/providers/medusa/auth.js +601 -0
- package/src/providers/medusa/auth.ts +496 -0
- package/src/providers/medusa/fulfillment-planner.ts +225 -0
- package/src/providers/medusa/index.js +2485 -0
- package/src/providers/medusa/index.ts +4239 -0
- package/src/providers/medusa/medusa.ts +8 -0
- package/src/providers/medusa/payment.js +265 -0
- package/src/providers/medusa/payment.ts +269 -0
- package/src/providers/medusa/transforms.js +505 -0
- package/src/providers/medusa/transforms.ts +625 -0
- package/src/providers/shopify/index.ts +444 -0
- package/src/providers/shopify/mutations.ts +102 -0
- package/src/providers/shopify/queries.ts +324 -0
- package/src/providers/shopify/shopify.ts +7 -0
- package/src/providers/shopify/transforms.ts +270 -0
- package/src/providers/spree/index.ts +344 -0
- package/src/providers/spree/spree.ts +5 -0
- package/src/providers/spree/transforms.ts +254 -0
- package/src/providers/woocommerce/index.ts +310 -0
- package/src/providers/woocommerce/transforms.ts +254 -0
- package/src/providers/woocommerce/woocommerce.ts +5 -0
- package/src/resilience/README.md +299 -0
- package/src/resilience/circuit-breaker.ts +222 -0
- package/src/resilience/index.ts +10 -0
- package/src/types/capabilities.types.js +141 -0
- package/src/types/capabilities.types.ts +366 -0
- package/src/types/index.ts +24 -0
- package/src/types/provider.js +9 -0
- package/src/types/provider.ts +9 -0
- package/src/types/ui-responses.ts +261 -0
- package/src/utils/city-coordinates.ts +40 -0
- package/src/utils/geolocation.ts +52 -0
- package/src/utils/payment-utils.js +152 -0
- package/src/utils/payment-utils.ts +203 -0
- package/tsconfig.json +15 -0
- package/tsup.config.ts +14 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metrics Calculation Service
|
|
3
|
+
*
|
|
4
|
+
* Framework-agnostic business logic for calculating dashboard metrics
|
|
5
|
+
* from raw order and product data.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* - L2: Pure business logic, no framework dependencies
|
|
9
|
+
* - Works with @thorprovider/types Order and Product interfaces
|
|
10
|
+
* - Stateless static methods for easy testing
|
|
11
|
+
*
|
|
12
|
+
* @module admin/MetricsService
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { DashboardMetrics, ChartDataPoint } from '@thorprovider/types';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Raw order data for metrics calculation
|
|
19
|
+
*
|
|
20
|
+
* @remarks
|
|
21
|
+
* Minimal interface to avoid coupling to specific Order type fields.
|
|
22
|
+
* Accepts the common fields available across all commerce providers.
|
|
23
|
+
*/
|
|
24
|
+
export interface MetricsOrderInput {
|
|
25
|
+
total?: number;
|
|
26
|
+
status?: string;
|
|
27
|
+
created_at?: string;
|
|
28
|
+
createdAt?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Raw product data for metrics calculation
|
|
33
|
+
*/
|
|
34
|
+
export interface MetricsProductInput {
|
|
35
|
+
id: string;
|
|
36
|
+
variants?: Array<{
|
|
37
|
+
quantityAvailable?: number;
|
|
38
|
+
}>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Metrics Service
|
|
43
|
+
*
|
|
44
|
+
* Calculates dashboard analytics from raw commerce data.
|
|
45
|
+
* All methods are static and pure (no side effects).
|
|
46
|
+
*/
|
|
47
|
+
export class MetricsService {
|
|
48
|
+
/** Threshold for low stock alerts */
|
|
49
|
+
static readonly LOW_STOCK_THRESHOLD = 10;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Calculate complete dashboard metrics from orders and products
|
|
53
|
+
*
|
|
54
|
+
* @param orders - Array of order data
|
|
55
|
+
* @param products - Array of product data
|
|
56
|
+
* @returns Calculated DashboardMetrics
|
|
57
|
+
*/
|
|
58
|
+
static calculate(
|
|
59
|
+
orders: MetricsOrderInput[],
|
|
60
|
+
products: MetricsProductInput[],
|
|
61
|
+
): DashboardMetrics {
|
|
62
|
+
const now = new Date();
|
|
63
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
64
|
+
const weekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
65
|
+
const monthAgo = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
66
|
+
|
|
67
|
+
// Calculate sales
|
|
68
|
+
const totalSales = orders.reduce((sum, o) => sum + (o.total || 0), 0);
|
|
69
|
+
const todaySales = orders
|
|
70
|
+
.filter((o) => new Date(o.created_at || o.createdAt || '') >= today)
|
|
71
|
+
.reduce((sum, o) => sum + (o.total || 0), 0);
|
|
72
|
+
const weekSales = orders
|
|
73
|
+
.filter((o) => new Date(o.created_at || o.createdAt || '') >= weekAgo)
|
|
74
|
+
.reduce((sum, o) => sum + (o.total || 0), 0);
|
|
75
|
+
const monthSales = orders
|
|
76
|
+
.filter((o) => new Date(o.created_at || o.createdAt || '') >= monthAgo)
|
|
77
|
+
.reduce((sum, o) => sum + (o.total || 0), 0);
|
|
78
|
+
|
|
79
|
+
// Calculate orders by status
|
|
80
|
+
const ordersByStatus = {
|
|
81
|
+
total: orders.length,
|
|
82
|
+
pending: orders.filter((o) => o.status === 'pending').length,
|
|
83
|
+
processing: orders.filter((o) => o.status === 'processing').length,
|
|
84
|
+
completed: orders.filter((o) => o.status === 'completed').length,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Calculate products
|
|
88
|
+
const lowStockCount = products.filter((p) => {
|
|
89
|
+
const totalQty = (p.variants || []).reduce(
|
|
90
|
+
(sum, v) => sum + (v.quantityAvailable || 0),
|
|
91
|
+
0,
|
|
92
|
+
);
|
|
93
|
+
return totalQty < MetricsService.LOW_STOCK_THRESHOLD;
|
|
94
|
+
}).length;
|
|
95
|
+
|
|
96
|
+
const productsMetrics = {
|
|
97
|
+
total: products.length,
|
|
98
|
+
lowStock: lowStockCount,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Generate chart data (last 7 days)
|
|
102
|
+
const chartData = MetricsService.generateChartData(orders, today);
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
sales: {
|
|
106
|
+
total: totalSales,
|
|
107
|
+
today: todaySales,
|
|
108
|
+
week: weekSales,
|
|
109
|
+
month: monthSales,
|
|
110
|
+
},
|
|
111
|
+
orders: ordersByStatus,
|
|
112
|
+
products: productsMetrics,
|
|
113
|
+
chartData,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Generate chart data for the last 7 days
|
|
119
|
+
*/
|
|
120
|
+
private static generateChartData(
|
|
121
|
+
orders: MetricsOrderInput[],
|
|
122
|
+
today: Date,
|
|
123
|
+
): ChartDataPoint[] {
|
|
124
|
+
const chartData: ChartDataPoint[] = [];
|
|
125
|
+
|
|
126
|
+
for (let i = 6; i >= 0; i--) {
|
|
127
|
+
const date = new Date(today.getTime() - i * 24 * 60 * 60 * 1000);
|
|
128
|
+
const nextDate = new Date(date.getTime() + 24 * 60 * 60 * 1000);
|
|
129
|
+
|
|
130
|
+
const daySales = orders
|
|
131
|
+
.filter((o) => {
|
|
132
|
+
const orderDate = new Date(o.created_at || o.createdAt || '');
|
|
133
|
+
return orderDate >= date && orderDate < nextDate;
|
|
134
|
+
})
|
|
135
|
+
.reduce((sum, o) => sum + (o.total || 0), 0);
|
|
136
|
+
|
|
137
|
+
chartData.push({
|
|
138
|
+
date: date.toISOString().split('T')[0],
|
|
139
|
+
sales: daySales,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return chartData;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin Services
|
|
3
|
+
*
|
|
4
|
+
* Central export point for all admin-related services.
|
|
5
|
+
*
|
|
6
|
+
* @module admin
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export { AdminAuthService } from './AdminAuthService';
|
|
10
|
+
export { AdminSessionService } from './AdminSessionService';
|
|
11
|
+
export type { AdminSession } from './AdminSessionService';
|
|
12
|
+
export { MetricsService } from './MetricsService';
|
|
13
|
+
export type { MetricsOrderInput, MetricsProductInput } from './MetricsService';
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend Metadata Adapter Interface
|
|
3
|
+
*
|
|
4
|
+
* Framework-agnostic interface for storing configuration in backend metadata fields.
|
|
5
|
+
* This replaces the need for a separate database (SQLite/Supabase).
|
|
6
|
+
*
|
|
7
|
+
* Each backend (Medusa, Shopify, WordPress) implements this interface
|
|
8
|
+
* using their native metadata/metafields/postmeta capabilities.
|
|
9
|
+
*
|
|
10
|
+
* @module backend/BackendMetadataAdapter
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Backend Metadata Adapter
|
|
15
|
+
*
|
|
16
|
+
* @remarks
|
|
17
|
+
* Supported resource types by backend:
|
|
18
|
+
* - Medusa: `'sales_channel'`, `'customer'`, `'order'` (via admin API)
|
|
19
|
+
* - Shopify: Uses metafields API (TBD)
|
|
20
|
+
* - WordPress: Uses post_meta or user_meta (TBD)
|
|
21
|
+
*
|
|
22
|
+
* Multi-tenant channel scoping:
|
|
23
|
+
* Use with `'customer'` / `'order'` resource types and the well-known key
|
|
24
|
+
* `'sales_channel_id'` to tag entities to a specific sales channel.
|
|
25
|
+
* See `@thorprovider/adapters/metadata/channel-scope` for helper utilities.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* // Store site configuration in Medusa sales channel metadata
|
|
30
|
+
* await adapter.setMetadata(
|
|
31
|
+
* 'sales_channel',
|
|
32
|
+
* 'sc_01HXXX',
|
|
33
|
+
* 'site_config',
|
|
34
|
+
* { version: '2026-03-25', config: {...} }
|
|
35
|
+
* );
|
|
36
|
+
*
|
|
37
|
+
* // Tag a customer to a sales channel (multi-tenant)
|
|
38
|
+
* await adapter.setMetadata(
|
|
39
|
+
* 'customer',
|
|
40
|
+
* 'cus_01HXXX',
|
|
41
|
+
* 'sales_channel_id',
|
|
42
|
+
* 'sc_01HXXX'
|
|
43
|
+
* );
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export interface BackendMetadataAdapter {
|
|
47
|
+
/**
|
|
48
|
+
* Get metadata value from backend resource
|
|
49
|
+
*
|
|
50
|
+
* @param resourceType - Type of resource (`'sales_channel'` | `'customer'` | `'order'` | `'store'`)
|
|
51
|
+
* @param resourceId - ID of the resource
|
|
52
|
+
* @param key - Metadata key (e.g., `'site_config'`, `'sales_channel_id'`)
|
|
53
|
+
* @returns Metadata value or null if not found
|
|
54
|
+
*/
|
|
55
|
+
getMetadata(
|
|
56
|
+
resourceType: string,
|
|
57
|
+
resourceId: string,
|
|
58
|
+
key: string
|
|
59
|
+
): Promise<any | null>;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Set metadata value on backend resource
|
|
63
|
+
*
|
|
64
|
+
* @param resourceType - Type of resource
|
|
65
|
+
* @param resourceId - ID of the resource
|
|
66
|
+
* @param key - Metadata key
|
|
67
|
+
* @param value - Metadata value (will be JSON serialized)
|
|
68
|
+
*/
|
|
69
|
+
setMetadata(
|
|
70
|
+
resourceType: string,
|
|
71
|
+
resourceId: string,
|
|
72
|
+
key: string,
|
|
73
|
+
value: any
|
|
74
|
+
): Promise<void>;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Delete metadata key from backend resource
|
|
78
|
+
*
|
|
79
|
+
* @param resourceType - Type of resource
|
|
80
|
+
* @param resourceId - ID of the resource
|
|
81
|
+
* @param key - Metadata key
|
|
82
|
+
*/
|
|
83
|
+
deleteMetadata(
|
|
84
|
+
resourceType: string,
|
|
85
|
+
resourceId: string,
|
|
86
|
+
key: string
|
|
87
|
+
): Promise<void>;
|
|
88
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend Metadata Adapter Factory
|
|
3
|
+
*
|
|
4
|
+
* Framework-agnostic factory to create backend metadata adapters for admin/dashboard.
|
|
5
|
+
* Keeps vendor-specific code in L2, prevents lock-in in L5.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* This factory is distinct from the main provider factory (createProvider).
|
|
9
|
+
* It's used for admin operations that require metadata/catalog access.
|
|
10
|
+
* Provider types are imported from @thorprovider/types (L1) to maintain sync.
|
|
11
|
+
*
|
|
12
|
+
* @module backend/factory
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import Medusa from '@medusajs/js-sdk';
|
|
16
|
+
import { MedusaMetadataAdapter } from './medusa/MedusaMetadataAdapter';
|
|
17
|
+
import type { BackendMetadataAdapter } from './BackendMetadataAdapter';
|
|
18
|
+
import type { ProviderTypeString } from '@thorprovider/types';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Configuration for creating a backend metadata adapter
|
|
22
|
+
*
|
|
23
|
+
* @remarks
|
|
24
|
+
* Currently only 'medusa' is implemented. When adding Shopify/WooCommerce metadata adapters:
|
|
25
|
+
* 1. Uncomment the provider type in packages/types/src/provider.ts
|
|
26
|
+
* 2. Update this interface to accept the new provider type
|
|
27
|
+
* 3. Implement ShopifyMetadataAdapter, WooCommerceMetadataAdapter
|
|
28
|
+
* 4. Add cases to createBackendMetadataAdapter() switch
|
|
29
|
+
*/
|
|
30
|
+
export interface BackendMetadataAdapterConfig {
|
|
31
|
+
/** Backend provider type (from @thorprovider/types L1) */
|
|
32
|
+
provider: ProviderTypeString;
|
|
33
|
+
|
|
34
|
+
/** Backend base URL */
|
|
35
|
+
baseUrl: string;
|
|
36
|
+
|
|
37
|
+
/** Publishable API key (for storefront access) */
|
|
38
|
+
publishableKey?: string;
|
|
39
|
+
|
|
40
|
+
/** Enable debug logging */
|
|
41
|
+
debug?: boolean;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Secret admin API key (from COMMERCE_ADMIN_API_KEY).
|
|
45
|
+
* When provided, the metadata adapter SDK instance will send
|
|
46
|
+
* `x-medusa-access-token` header on all admin requests.
|
|
47
|
+
* Required for getMetadata/setMetadata to work after removing email/password auth.
|
|
48
|
+
*/
|
|
49
|
+
adminApiKey?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create a backend metadata adapter (Factory Pattern)
|
|
54
|
+
*
|
|
55
|
+
* Keeps vendor-specific code in L2, prevents lock-in in L5.
|
|
56
|
+
* L5 only provides configuration from env vars.
|
|
57
|
+
*
|
|
58
|
+
* @param config - Adapter configuration
|
|
59
|
+
* @returns BackendMetadataAdapter instance
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```typescript
|
|
63
|
+
* // In L5 (starters)
|
|
64
|
+
* import { createBackendMetadataAdapter } from '@thorprovider/adapters';
|
|
65
|
+
*
|
|
66
|
+
* export const metadataAdapter = createBackendMetadataAdapter({
|
|
67
|
+
* provider: process.env.NEXT_PUBLIC_COMMERCE_PROVIDER as any,
|
|
68
|
+
* baseUrl: process.env.NEXT_PUBLIC_COMMERCE_API_URL!,
|
|
69
|
+
* publishableKey: process.env.NEXT_PUBLIC_COMMERCE_API_KEY!,
|
|
70
|
+
* });
|
|
71
|
+
* ```
|
|
72
|
+
*
|
|
73
|
+
* @throws {Error} If provider is not supported
|
|
74
|
+
*/
|
|
75
|
+
export function createBackendMetadataAdapter(
|
|
76
|
+
config: BackendMetadataAdapterConfig
|
|
77
|
+
): BackendMetadataAdapter {
|
|
78
|
+
const { provider, baseUrl, publishableKey, debug } = config;
|
|
79
|
+
|
|
80
|
+
if (provider === 'medusa') {
|
|
81
|
+
const { adminApiKey } = config;
|
|
82
|
+
|
|
83
|
+
// Create Medusa SDK client instance
|
|
84
|
+
const medusa = new Medusa({
|
|
85
|
+
baseUrl,
|
|
86
|
+
publishableKey,
|
|
87
|
+
debug: debug ?? false,
|
|
88
|
+
// When adminApiKey is provided: sends x-medusa-access-token on all requests.
|
|
89
|
+
// When not provided: requests are unauthenticated — admin routes will 401.
|
|
90
|
+
...(adminApiKey && { apiKey: adminApiKey }),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return new MedusaMetadataAdapter(medusa);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (provider === 'shopify') {
|
|
97
|
+
// TODO: Implement Shopify metadata adapter
|
|
98
|
+
throw new Error('Shopify metadata adapter not yet implemented');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (provider === 'woocommerce') {
|
|
102
|
+
// TODO: Implement WooCommerce metadata adapter
|
|
103
|
+
throw new Error('WooCommerce metadata adapter not yet implemented');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
throw new Error(`Unsupported provider: ${provider}`);
|
|
107
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend Metadata Adapters
|
|
3
|
+
*
|
|
4
|
+
* Central export point for all backend metadata adapters.
|
|
5
|
+
* Framework-agnostic factory pattern to prevent vendor lock-in in L5.
|
|
6
|
+
*
|
|
7
|
+
* @module backend
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export * from './BackendMetadataAdapter';
|
|
11
|
+
export * from './factory';
|
|
12
|
+
export * from './medusa/MedusaMetadataAdapter';
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Medusa Metadata Adapter
|
|
3
|
+
*
|
|
4
|
+
* Implementation of BackendMetadataAdapter for Medusa.js v2.
|
|
5
|
+
* Uses Medusa's native metadata fields on sales_channel, customer, and order resources.
|
|
6
|
+
*
|
|
7
|
+
* Supported resource types:
|
|
8
|
+
* - `'sales_channel'` — site config, channel-level settings
|
|
9
|
+
* - `'customer'` — multi-tenant scoping (`sales_channel_id` key)
|
|
10
|
+
* - `'order'` — multi-tenant scoping (`sales_channel_id` key)
|
|
11
|
+
*
|
|
12
|
+
* All operations require admin-level JWT authentication.
|
|
13
|
+
*
|
|
14
|
+
* @module backend/medusa/MedusaMetadataAdapter
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type Medusa from '@medusajs/js-sdk';
|
|
18
|
+
import type { BackendMetadataAdapter } from '../BackendMetadataAdapter';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Medusa Metadata Adapter
|
|
22
|
+
*
|
|
23
|
+
* @remarks
|
|
24
|
+
* - Supports 'sales_channel' and 'store' resource types
|
|
25
|
+
* - Uses Medusa admin API (requires admin token)
|
|
26
|
+
* - Metadata is stored as JSON in the metadata field
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* const adapter = new MedusaMetadataAdapter(medusaClient);
|
|
31
|
+
*
|
|
32
|
+
* // Store site configuration
|
|
33
|
+
* await adapter.setMetadata(
|
|
34
|
+
* 'sales_channel',
|
|
35
|
+
* 'sc_01HXXX',
|
|
36
|
+
* 'site_config',
|
|
37
|
+
* { version: '2026-03-25', config: {...} }
|
|
38
|
+
* );
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export class MedusaMetadataAdapter implements BackendMetadataAdapter {
|
|
42
|
+
constructor(private sdk: Medusa) {}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get metadata value from Medusa resource
|
|
46
|
+
*/
|
|
47
|
+
async getMetadata(
|
|
48
|
+
resourceType: string,
|
|
49
|
+
resourceId: string,
|
|
50
|
+
key: string
|
|
51
|
+
): Promise<any | null> {
|
|
52
|
+
try {
|
|
53
|
+
if (resourceType === 'sales_channel') {
|
|
54
|
+
const { sales_channel } = await this.sdk.admin.salesChannel.retrieve(
|
|
55
|
+
resourceId
|
|
56
|
+
);
|
|
57
|
+
return sales_channel.metadata?.[key] ?? null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (resourceType === 'customer') {
|
|
61
|
+
const result = await (this.sdk as any).admin.customer.retrieve(resourceId);
|
|
62
|
+
return result?.customer?.metadata?.[key] ?? null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (resourceType === 'order') {
|
|
66
|
+
const result = await (this.sdk as any).admin.order.retrieve(resourceId);
|
|
67
|
+
return result?.order?.metadata?.[key] ?? null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Note: Store API in Medusa v2 requires store ID
|
|
71
|
+
// Store support can be added when we have a reliable way to get the store ID
|
|
72
|
+
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Unsupported resource type: ${resourceType}. ` +
|
|
75
|
+
`Supported: 'sales_channel', 'customer', 'order'.`
|
|
76
|
+
);
|
|
77
|
+
} catch (error: any) {
|
|
78
|
+
// If resource not found or metadata key doesn't exist, return null
|
|
79
|
+
if (error.response?.status === 404 || error.status === 404) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Set metadata value on Medusa resource
|
|
88
|
+
*/
|
|
89
|
+
async setMetadata(
|
|
90
|
+
resourceType: string,
|
|
91
|
+
resourceId: string,
|
|
92
|
+
key: string,
|
|
93
|
+
value: any
|
|
94
|
+
): Promise<void> {
|
|
95
|
+
if (resourceType === 'sales_channel') {
|
|
96
|
+
await this.sdk.admin.salesChannel.update(resourceId, {
|
|
97
|
+
metadata: { [key]: value },
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (resourceType === 'customer') {
|
|
103
|
+
await (this.sdk as any).admin.customer.update(resourceId, {
|
|
104
|
+
metadata: { [key]: value },
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (resourceType === 'order') {
|
|
110
|
+
await (this.sdk as any).admin.order.update(resourceId, {
|
|
111
|
+
metadata: { [key]: value },
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Note: Store API in Medusa v2 requires store ID
|
|
117
|
+
// Store support can be added when we have a reliable way to get the store ID
|
|
118
|
+
|
|
119
|
+
throw new Error(
|
|
120
|
+
`Unsupported resource type: ${resourceType}. ` +
|
|
121
|
+
`Supported: 'sales_channel', 'customer', 'order'.`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Delete metadata key from Medusa resource
|
|
127
|
+
*
|
|
128
|
+
* @remarks
|
|
129
|
+
* Medusa deletes metadata keys by setting them to undefined or null
|
|
130
|
+
*/
|
|
131
|
+
async deleteMetadata(
|
|
132
|
+
resourceType: string,
|
|
133
|
+
resourceId: string,
|
|
134
|
+
key: string
|
|
135
|
+
): Promise<void> {
|
|
136
|
+
// Set to null to delete the key
|
|
137
|
+
await this.setMetadata(resourceType, resourceId, key, null);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
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
|
+
}
|