@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,225 @@
|
|
|
1
|
+
# Multi-Tenant Channel Scoping
|
|
2
|
+
|
|
3
|
+
> Backend-agnostic pattern for isolating entities by sales channel using metadata.
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
Thor Commerce deploys one V0 Starter per sales channel (multi-tenant).
|
|
8
|
+
Products and carts are natively scoped by Medusa's `sales_channel_id`, but
|
|
9
|
+
**customers, orders, and categories are not**. A dropshipper must only see
|
|
10
|
+
their own customers — never those of another channel or the main store.
|
|
11
|
+
|
|
12
|
+
## Solution: Metadata-Based Channel Scope
|
|
13
|
+
|
|
14
|
+
Tag un-scoped entities with a well-known metadata key:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
entity.metadata.sales_channel_id = "sc_01HXXX"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Then filter at the adapter level (L2) before data reaches L3–L5.
|
|
21
|
+
|
|
22
|
+
### Why metadata?
|
|
23
|
+
|
|
24
|
+
| Criteria | Metadata | Custom DB field |
|
|
25
|
+
|-----------------------|-------------------|-----------------------|
|
|
26
|
+
| Zero migrations | ✅ | ❌ per backend |
|
|
27
|
+
| Works across backends | ✅ (Medusa, Shopify metafields, WC meta) | ❌ vendor-specific |
|
|
28
|
+
| Writable via SDK | ✅ admin API | ❌ requires backend deploy |
|
|
29
|
+
| Type-safe | ⚠️ runtime validation | ✅ DB schema |
|
|
30
|
+
| Query-indexed | ❌ in-memory filter | ✅ DB index |
|
|
31
|
+
|
|
32
|
+
For most stores (<10k entities per type) in-memory filtering is acceptable.
|
|
33
|
+
For larger catalogs, consider adding a DB index on
|
|
34
|
+
`customer.metadata->>'sales_channel_id'` in your Medusa backend.
|
|
35
|
+
|
|
36
|
+
## Architecture
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
L1 (@thorprovider/types)
|
|
40
|
+
└─ Customer.metadata, Order.metadata, GetCustomersOptions.salesChannelId
|
|
41
|
+
|
|
42
|
+
L2 (@thorprovider/adapters)
|
|
43
|
+
├─ metadata/channel-scope.ts ← Pure helpers (build, read, filter)
|
|
44
|
+
├─ metadata/channel-scope-validation.ts ← Validation (no external deps)
|
|
45
|
+
├─ backend/MedusaMetadataAdapter ← Read/write metadata per resource type
|
|
46
|
+
└─ providers/medusa/index.ts ← assignCustomerToChannel(), filterByChannel()
|
|
47
|
+
|
|
48
|
+
L5 (V0 Starter)
|
|
49
|
+
└─ API routes / server actions call assignCustomerToChannel() post-registration
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
### Assign a customer to a channel
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// In L5 API route (post-registration)
|
|
58
|
+
const customer = await commerce.assignCustomerToChannel(
|
|
59
|
+
customerId,
|
|
60
|
+
process.env.NEXT_PUBLIC_SALES_CHANNEL_ID!,
|
|
61
|
+
'system', // optional: who assigned
|
|
62
|
+
);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### List customers scoped to a channel
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// Dropshipper view — only their customers
|
|
69
|
+
const customers = await commerce.getCustomers({
|
|
70
|
+
salesChannelId: process.env.NEXT_PUBLIC_SALES_CHANNEL_ID,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Admin view — all customers
|
|
74
|
+
const allCustomers = await commerce.getCustomers();
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Assign an order to a channel
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
await commerce.assignOrderToChannel(
|
|
81
|
+
orderId,
|
|
82
|
+
process.env.NEXT_PUBLIC_SALES_CHANNEL_ID!,
|
|
83
|
+
'checkout-flow',
|
|
84
|
+
);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Use helpers directly (any entity)
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import {
|
|
91
|
+
buildChannelScopeMetadata,
|
|
92
|
+
filterByChannel,
|
|
93
|
+
getChannelScope,
|
|
94
|
+
isScopedToChannel,
|
|
95
|
+
partitionByChannel,
|
|
96
|
+
} from '@thorprovider/adapters';
|
|
97
|
+
|
|
98
|
+
// Build metadata payload
|
|
99
|
+
const meta = buildChannelScopeMetadata('sc_123', 'admin@store.com');
|
|
100
|
+
// → { sales_channel_id: 'sc_123', channel_assigned_at: '2026-03-27T...', channel_assigned_by: 'admin@store.com' }
|
|
101
|
+
|
|
102
|
+
// Filter an array of entities
|
|
103
|
+
const myCustomers = filterByChannel(allCustomers, 'sc_123');
|
|
104
|
+
|
|
105
|
+
// Check one entity
|
|
106
|
+
if (isScopedToChannel(customer, 'sc_123')) { /* ... */ }
|
|
107
|
+
|
|
108
|
+
// Partition into scoped + global
|
|
109
|
+
const { scoped, global } = partitionByChannel(customers, 'sc_123');
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Validation
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
import {
|
|
116
|
+
validateChannelScopeAssignment,
|
|
117
|
+
assertEntityBelongsToChannel,
|
|
118
|
+
ChannelScopeValidationError,
|
|
119
|
+
} from '@thorprovider/adapters';
|
|
120
|
+
|
|
121
|
+
// Validates all inputs before assignment
|
|
122
|
+
validateChannelScopeAssignment(customerId, channelId, assignedBy);
|
|
123
|
+
|
|
124
|
+
// Guard: ensure entity belongs to caller's channel
|
|
125
|
+
try {
|
|
126
|
+
assertEntityBelongsToChannel(customer, myChannelId);
|
|
127
|
+
} catch (e) {
|
|
128
|
+
if (e instanceof ChannelScopeValidationError) {
|
|
129
|
+
return res.status(403).json({ error: 'Access denied' });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Extending to New Entity Types
|
|
135
|
+
|
|
136
|
+
The pattern is generic. To scope a new entity type:
|
|
137
|
+
|
|
138
|
+
1. **Ensure** the entity type in `@thorprovider/types` has `metadata?: Record<string, unknown>`
|
|
139
|
+
2. **Add** the resource type to `MedusaMetadataAdapter` (`getMetadata` / `setMetadata`)
|
|
140
|
+
3. **Add** a `salesChannelId` filter option to the entity's query options type
|
|
141
|
+
4. **Call** `filterByChannel()` in the provider's list method
|
|
142
|
+
5. **Optionally** add an `assignXToChannel()` convenience method
|
|
143
|
+
|
|
144
|
+
### Currently supported
|
|
145
|
+
|
|
146
|
+
| Entity | Native scoping (Medusa) | Metadata scoping (Thor) |
|
|
147
|
+
|-----------------|------------------------|------------------------|
|
|
148
|
+
| Products | ✅ `sales_channel_id` | N/A (native) |
|
|
149
|
+
| Carts | ✅ `sales_channel_id` | N/A (native) |
|
|
150
|
+
| Categories | ❌ | ⚠️ Via product cross-ref |
|
|
151
|
+
| **Customers** | ❌ | ✅ `metadata.sales_channel_id` |
|
|
152
|
+
| **Orders** | ❌ | ✅ `metadata.sales_channel_id` |
|
|
153
|
+
| Collections | ❌ (linked via products) | Planned |
|
|
154
|
+
|
|
155
|
+
## When to Assign Channel Scope
|
|
156
|
+
|
|
157
|
+
| Event | Action |
|
|
158
|
+
|----------------------------|---------------------------------------------|
|
|
159
|
+
| Customer registers | `assignCustomerToChannel()` post-registration |
|
|
160
|
+
| Dropshipper creates customer | `assignCustomerToChannel()` in admin flow |
|
|
161
|
+
| Order completed | `assignOrderToChannel()` in checkout flow |
|
|
162
|
+
| Admin reassigns entity | `assignXToChannel()` from admin UI |
|
|
163
|
+
|
|
164
|
+
## Backend-Specific Notes
|
|
165
|
+
|
|
166
|
+
### Medusa v2
|
|
167
|
+
|
|
168
|
+
- Uses `admin.customer.update(id, { metadata })` — requires admin JWT
|
|
169
|
+
- Uses `admin.order.update(id, { metadata })` — requires admin JWT
|
|
170
|
+
- Metadata is a JSONB column — no size limit in practice
|
|
171
|
+
- **Not indexed** by default; add a GIN index if filtering >10k entities
|
|
172
|
+
|
|
173
|
+
### Shopify (planned)
|
|
174
|
+
|
|
175
|
+
- Would use Customer Metafields API
|
|
176
|
+
- Key: `thor_commerce.sales_channel_id`
|
|
177
|
+
- Namespace isolates Thor metadata from other apps
|
|
178
|
+
|
|
179
|
+
### WooCommerce (planned)
|
|
180
|
+
|
|
181
|
+
- Would use `wp_usermeta` table
|
|
182
|
+
- Key: `_thor_sales_channel_id`
|
|
183
|
+
- Prefix `_` hides from admin UI by convention
|
|
184
|
+
|
|
185
|
+
## Optional: Custom Fetcher Callbacks
|
|
186
|
+
|
|
187
|
+
For backends that implement custom scoping endpoints (e.g., Medusa Module Link or custom API routes),
|
|
188
|
+
use the optional `customFetcher` callback in query options:
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
// Signature: override default SDK logic with custom endpoint
|
|
192
|
+
const customFetcher = async (options: GetCustomersOptions) => {
|
|
193
|
+
const channelId = options.salesChannelId;
|
|
194
|
+
if (!channelId) throw new Error('Channel scoping required for custom fetcher');
|
|
195
|
+
|
|
196
|
+
// Call custom endpoint instead of SDK
|
|
197
|
+
const response = await fetch(`/api/admin/sales-channels/${channelId}/customers`);
|
|
198
|
+
return response.json();
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// Usage in L5 (V0 Starter)
|
|
202
|
+
const customers = await commerce.getCustomers({
|
|
203
|
+
salesChannelId: 'sc_123',
|
|
204
|
+
customFetcher, // ← Uses callback instead of Medusa SDK
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Benefits
|
|
209
|
+
|
|
210
|
+
| Scenario | Without callback | With callback |
|
|
211
|
+
|----------|------------------|---------------|
|
|
212
|
+
| Medusa Module Link | Requires wrapper function in L5 | `customFetcher` passed at query time |
|
|
213
|
+
| Custom API endpoint | SDK → in-memory filter | Direct server-side filtering |
|
|
214
|
+
| Shopify (future) | Would need separate method | Uniform interface across backends |
|
|
215
|
+
|
|
216
|
+
The callback pattern defers implementation complexity while maintaining type safety.
|
|
217
|
+
Backends can adopt it independently without changing L3–L5 code.
|
|
218
|
+
|
|
219
|
+
## Security Considerations
|
|
220
|
+
|
|
221
|
+
- Channel assignment requires **admin-level authentication**
|
|
222
|
+
- `getCustomers({ salesChannelId })` filters in-memory after fetch (or via callback if provided)
|
|
223
|
+
- Defense-in-depth: combine with JWT scope where available
|
|
224
|
+
- `assertEntityBelongsToChannel()` provides explicit ownership checks
|
|
225
|
+
- Custom fetchers should **validate `salesChannelId`** before calling backend
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Profile Management
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
type: how-to
|
|
5
|
+
tags: [profile, account, customer, metadata, avatar]
|
|
6
|
+
related:
|
|
7
|
+
- authentication.md
|
|
8
|
+
- capabilities-system.md
|
|
9
|
+
- services-overview.md
|
|
10
|
+
updated: 2026-02-28
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
Guide for managing customer profiles using capability-aware patterns for cross-backend compatibility.
|
|
14
|
+
|
|
15
|
+
## Prerequisites
|
|
16
|
+
|
|
17
|
+
- [Authentication set up](authentication.md)
|
|
18
|
+
- User logged in to access profile features
|
|
19
|
+
|
|
20
|
+
## Quick Example
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { commerce } from '@/lib/commerce';
|
|
24
|
+
|
|
25
|
+
// Simple profile update
|
|
26
|
+
const updated = await commerce.auth.updateCustomer({
|
|
27
|
+
first_name: 'Thor',
|
|
28
|
+
last_name: 'Developer',
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Step-by-Step Instructions
|
|
33
|
+
|
|
34
|
+
### Step 1: Check Backend Capabilities
|
|
35
|
+
|
|
36
|
+
Always verify what the backend supports before rendering forms or performing updates.
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
const { updateEmail, updateProfile, deleteAccount } = commerce.capabilities;
|
|
40
|
+
|
|
41
|
+
if (updateEmail) {
|
|
42
|
+
// Render email update field
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Step 2: Retrieve Current Profile
|
|
47
|
+
|
|
48
|
+
Fetch the customer object to populate your forms.
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
const customer = await commerce.auth.getCurrentCustomer();
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Step 3: Implement Update Logic
|
|
55
|
+
|
|
56
|
+
Handle updates carefully, checking for specific field support.
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
async function handleUpdate(formData) {
|
|
60
|
+
const updateData = {};
|
|
61
|
+
|
|
62
|
+
if (commerce.capabilities.updateProfile) {
|
|
63
|
+
updateData.first_name = formData.firstName;
|
|
64
|
+
updateData.last_name = formData.lastName;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (commerce.capabilities.updateEmail) {
|
|
68
|
+
updateData.email = formData.email;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return await commerce.auth.updateCustomer(updateData);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Step 4: Manage Password & Security
|
|
76
|
+
|
|
77
|
+
Handle password changes based on whether the backend supports direct or email-based resets.
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
if (commerce.capabilities.changePasswordDirect) {
|
|
81
|
+
await commerce.auth.updateCustomer({
|
|
82
|
+
current_password: 'old',
|
|
83
|
+
password: 'new',
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## ✅ DO's
|
|
89
|
+
|
|
90
|
+
1. **Check Capabilities First:** Avoid rendering buttons for features the backend doesn't support.
|
|
91
|
+
2. **Use Metadata for Extensions:** Store extra info like `avatar_url` or `notifications` in `customer.metadata`.
|
|
92
|
+
3. **Handle Errors explicitly:** Distinguish between `EMAIL_TAKEN` and generic update errors.
|
|
93
|
+
4. **Validate on Client:** Use patterns/regex for phone and email before sending to the adapter.
|
|
94
|
+
5. **Protect UI with Loading States:** Backend operations can take time; show a spinner.
|
|
95
|
+
|
|
96
|
+
## ❌ DONT's
|
|
97
|
+
|
|
98
|
+
1. **Don't assume all backends are equal:** Medusa v2 has different capabilities than Shopify or WooCommerce.
|
|
99
|
+
2. **Don't hardcode profile fields:** Use the `Customer` interface from `@thorprovider/adapters`.
|
|
100
|
+
3. **Don't ignore account deletion:** Provide a way for users to delete their account if `deleteAccount` is true.
|
|
101
|
+
4. **Don't send sensitive data in metadata:** Metadata is often visible to the user and sometimes less secure.
|
|
102
|
+
5. **Don't forget to re-fetch profile:** After a successful update, the local customer state should be refreshed.
|
|
103
|
+
|
|
104
|
+
## Common Patterns
|
|
105
|
+
|
|
106
|
+
### Metadata-based Avatar Management
|
|
107
|
+
Since many backends don't support native avatars, use an external service (S3/Cloudinary) and store the URL in metadata.
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
await commerce.auth.updateCustomer({
|
|
111
|
+
metadata: { avatar_url: 'https://...' }
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Adaptive Profile Form
|
|
116
|
+
Render only the fields supported by the current provider.
|
|
117
|
+
|
|
118
|
+
## Common Errors & Solutions
|
|
119
|
+
|
|
120
|
+
### Error: `OPERATION_NOT_SUPPORTED`
|
|
121
|
+
**Cause:** Attempting to update a field (like email) on a backend that doesn't allow it.
|
|
122
|
+
**Solution:** Check `commerce.capabilities` before calling the method.
|
|
123
|
+
|
|
124
|
+
### Error: `EMAIL_TAKEN`
|
|
125
|
+
**Cause:** User tried to change email to one already registered.
|
|
126
|
+
**Solution:** Show a "This email is already in use" validation message.
|
|
127
|
+
|
|
128
|
+
## Common Pitfalls
|
|
129
|
+
|
|
130
|
+
> ⚠️ **Pitfall 1: Metadata Overwrites**
|
|
131
|
+
> Some backends overwrite the entire metadata object instead of merging. Always fetch the current ones first if you want to preserve existing fields.
|
|
132
|
+
|
|
133
|
+
> ⚠️ **Pitfall 2: Password Complexity**
|
|
134
|
+
> Backends have different password requirements. Let the backend validate and handle the error message.
|
|
135
|
+
|
|
136
|
+
## Troubleshooting
|
|
137
|
+
|
|
138
|
+
| Issue | Solution |
|
|
139
|
+
|-------|----------|
|
|
140
|
+
| Profile data not saving | Check if the user session is still active |
|
|
141
|
+
| Metadata field missing | Ensure the key exists in the backend's allowed metadata list |
|
|
142
|
+
| 403 Forbidden | User might not have permission to update their own profile |
|
|
143
|
+
|
|
144
|
+
## Next Steps
|
|
145
|
+
|
|
146
|
+
- [Capabilities System](capabilities-system.md)
|
|
147
|
+
- [Authentication](authentication.md)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Providers Overview
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
type: guide
|
|
5
|
+
tags: [architecture, providers, medusa, shopify, roadmap]
|
|
6
|
+
related:
|
|
7
|
+
- capabilities-system.md
|
|
8
|
+
- services-overview.md
|
|
9
|
+
- getting-started.md
|
|
10
|
+
updated: 2026-02-28
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
Overview of the e-commerce platforms supported by Thor Adapters and how they are abstracted into a unified interface.
|
|
14
|
+
|
|
15
|
+
## Overview
|
|
16
|
+
|
|
17
|
+
Thor Adapters use a "Provider" architecture to connect our application with different e-commerce backends. Each provider acts as a **full compatibility layer**, handling both:
|
|
18
|
+
- **Inbound (Read)**: mapping vendor-specific API responses to the standardized Thor commerce types.
|
|
19
|
+
- **Outbound (Write)**: transforming `@thorprovider/types` objects into the vendor-specific API payloads required for mutations (creating carts, updating addresses, placing orders, managing customers, etc.).
|
|
20
|
+
|
|
21
|
+
## The Problem
|
|
22
|
+
|
|
23
|
+
E-commerce backends vary wildly in their API architecture (REST vs GraphQL), authentication methods (JWT, Cookies, OAuth), and feature sets.
|
|
24
|
+
- **Vendor Lock-in:** Migrating from one backend to another usually requires a total rewrite of the frontend data layer.
|
|
25
|
+
- **Maintenance Burden:** Supporting multiple backends often leads to fragmented codebases with many `if/else` checks for backend types.
|
|
26
|
+
|
|
27
|
+
Thor Adapters eliminate these problems by wrapping the data layer in a consistent interface.
|
|
28
|
+
|
|
29
|
+
## Current Support Matrix
|
|
30
|
+
|
|
31
|
+
### Medusa (v2.0)
|
|
32
|
+
**Status:** ✅ Production Ready
|
|
33
|
+
Our primary integration, highly optimized for headless performance.
|
|
34
|
+
|
|
35
|
+
### Shopify
|
|
36
|
+
**Status:** 🚧 In Development (Phase 1)
|
|
37
|
+
Focusing on Storefront API integration for product discovery and cart.
|
|
38
|
+
|
|
39
|
+
### WooCommerce
|
|
40
|
+
**Status:** 🗓️ Planned
|
|
41
|
+
Targeting the REST API for hybrid headless setups.
|
|
42
|
+
|
|
43
|
+
## Conceptual Foundation
|
|
44
|
+
|
|
45
|
+
### 1. The Strategy Pattern
|
|
46
|
+
We use the strategy pattern to swap implementation at runtime. Your application logic doesn't care if a product comes from Medusa or Shopify; it only sees the `Product` interface.
|
|
47
|
+
|
|
48
|
+
### 2. Capabilities-Driven Development
|
|
49
|
+
Instead of checking `if (provider === 'medusa')`, we check `if (commerce.capabilities.canAddMetadata)`. This makes the frontend future-proof as backends evolve.
|
|
50
|
+
|
|
51
|
+
## Architecture & Relationships
|
|
52
|
+
|
|
53
|
+
```mermaid
|
|
54
|
+
graph TD
|
|
55
|
+
A[createProvider Factory] --> B{Configuration}
|
|
56
|
+
B --> C[Medusa Provider]
|
|
57
|
+
B --> D[Shopify Provider]
|
|
58
|
+
B --> E[Mock Provider]
|
|
59
|
+
C --> F[Unified Commerce Interface]
|
|
60
|
+
D --> F
|
|
61
|
+
E --> F
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Design Decisions
|
|
65
|
+
|
|
66
|
+
- **Medusa-First:** Since Medusa v2 is our primary backend, our standardized types are heavily influenced by its structure (e.g., variant handling).
|
|
67
|
+
- **Graceful Degradation:** If a provider doesn't support a feature (like "Gift Cards"), the corresponding service returns a standardized `NotSupportedError`.
|
|
68
|
+
- **Statelessness:** Providers avoid local state to stay predictable in server-side contexts.
|
|
69
|
+
|
|
70
|
+
## Common Misconceptions
|
|
71
|
+
|
|
72
|
+
- **"All providers are identical"**: While the interface is identical, the *behavior* differs (e.g., login speed, pagination limits).
|
|
73
|
+
- **"I can easily switch in production"**: While the code supports it, data migration between backends is still a separate, complex manual task.
|
|
74
|
+
- **"Every provider uses GraphQL"**: We use whatever is most efficient for that backend (GraphQL for Shopify, REST for Medusa).
|
|
75
|
+
|
|
76
|
+
## Anti-Patterns
|
|
77
|
+
|
|
78
|
+
1. **Backend-Specific Logic in UI:** Avoid `if (commerce.name === 'medusa')` in your React components.
|
|
79
|
+
2. **Hardcoding Search Syntax:** Search syntax differs across providers; use the standardized `getProducts({ query: '...' })` method.
|
|
80
|
+
|
|
81
|
+
## Next Steps
|
|
82
|
+
|
|
83
|
+
- [Reference: Capabilities System](capabilities-system.md)
|
|
84
|
+
- [How-to: Getting Started](getting-started.md)
|
|
85
|
+
- [Workflow: Add New Provider](../../../docs/workflows/legacy/add-provider.md)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Services Overview
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
type: guide
|
|
5
|
+
tags: [architecture, services, commerce, api, architecture]
|
|
6
|
+
related:
|
|
7
|
+
- getting-started.md
|
|
8
|
+
- capabilities-system.md
|
|
9
|
+
- providers-overview.md
|
|
10
|
+
updated: 2026-02-28
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
Overview of the functional services provided by Thor adapters and how they interact to provide a unified e-commerce interface.
|
|
14
|
+
|
|
15
|
+
## Overview
|
|
16
|
+
|
|
17
|
+
Thor adapters organize commerce functionality into discrete services. This abstraction allows the rest of the application to remain backend-agnostic, interacting with a standardized set of methods regardless of whether the provider is Medusa, Shopify, or a custom backend.
|
|
18
|
+
|
|
19
|
+
## The Problem
|
|
20
|
+
|
|
21
|
+
Directly integrating with multiple commerce backends is difficult because:
|
|
22
|
+
- **Inconsistent Schemas:** Every API has different nesting and field names for a "Product".
|
|
23
|
+
- **Varied Authentication:** Some use JWT, some use Cookies, some use Oauth.
|
|
24
|
+
- **Feature Disparity:** Not all backends support "Related Products" or "Gift Cards".
|
|
25
|
+
|
|
26
|
+
Thor Adapters solve this by providing a unified service layer and a capabilities system to handle these differences gracefully.
|
|
27
|
+
|
|
28
|
+
## Core Services
|
|
29
|
+
|
|
30
|
+
### 1. Product Service
|
|
31
|
+
Responsible for retrieving product data, searching, and obtaining recommendations.
|
|
32
|
+
|
|
33
|
+
**Key capabilities:**
|
|
34
|
+
- Multi-variant support
|
|
35
|
+
- Standardized price ranges
|
|
36
|
+
- Cross-sell/Upsell recommendations
|
|
37
|
+
|
|
38
|
+
### 2. Collection Service
|
|
39
|
+
Handles grouping of products into categories or collections.
|
|
40
|
+
|
|
41
|
+
**Key capabilities:**
|
|
42
|
+
- Collection hierarchies
|
|
43
|
+
- Paginated results per collection
|
|
44
|
+
|
|
45
|
+
### 3. Cart Service
|
|
46
|
+
Manages the lifecycle of a shopping cart, from creation to checkout preparation.
|
|
47
|
+
|
|
48
|
+
**Key capabilities:**
|
|
49
|
+
- Unified line items
|
|
50
|
+
- Dynamic tax and total calculations
|
|
51
|
+
- Simple add/update/remove operations
|
|
52
|
+
|
|
53
|
+
#### Medusa v2 note: line item totals
|
|
54
|
+
In Medusa v2 Store API responses, cart line item totals such as `total`, `subtotal`, and `original_total` are only returned when you retrieve cart totals using the recommended query/expansions.
|
|
55
|
+
|
|
56
|
+
Thor Adapters treat `unit_price` as the reliable baseline (always present) and will fall back to computing a line total as `unit_price × quantity` when totals fields are missing. This prevents `$0.00` line items in the UI while still allowing accurate totals when Medusa returns them.
|
|
57
|
+
|
|
58
|
+
### 4. Authentication Service
|
|
59
|
+
Handles customer account management and secure sessions.
|
|
60
|
+
|
|
61
|
+
**Key capabilities:**
|
|
62
|
+
- JWT & Session support
|
|
63
|
+
- Profile and address management
|
|
64
|
+
- Metadata extensions
|
|
65
|
+
|
|
66
|
+
## Service Interactions
|
|
67
|
+
|
|
68
|
+
```mermaid
|
|
69
|
+
graph LR
|
|
70
|
+
A[App] --> B[Commerce Instance]
|
|
71
|
+
B --> C[Product Service]
|
|
72
|
+
B --> D[Cart Service]
|
|
73
|
+
B --> E[Auth Service]
|
|
74
|
+
C -.-> D
|
|
75
|
+
D -.-> E
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
1. **Discovery:** User explores products via Product Service.
|
|
79
|
+
2. **Action:** User adds to cart via Cart Service (which might link to the Customer).
|
|
80
|
+
3. **Checkout:** Cart Service redirects to the provider's checkout or prepares data for a custom one.
|
|
81
|
+
|
|
82
|
+
## Design Decisions
|
|
83
|
+
|
|
84
|
+
### Standardized Money Object
|
|
85
|
+
All prices are returned as a `Money` object with `amount` (string) and `currencyCode` to avoid floating-point errors.
|
|
86
|
+
|
|
87
|
+
### Universal Cursor-based Pagination
|
|
88
|
+
We use cursor-based pagination whenever possible to maintain compatibility with modern backends like Shopify and Medusa v2.
|
|
89
|
+
|
|
90
|
+
### Fail-fast with Typed Errors
|
|
91
|
+
Instead of returning `null` on errors, we throw a `CommerceError` with a specific `code`, allowing the UI to react appropriately.
|
|
92
|
+
|
|
93
|
+
## Common Misconceptions
|
|
94
|
+
|
|
95
|
+
- **"All methods are always available"**: Some backends don't support specific features. Always check `commerce.can('feature')`.
|
|
96
|
+
- **"Updates are immediate in the backend"**: Some providers use eventual consistency. Wait for the response before updating local state.
|
|
97
|
+
- **"The adapter stores state"**: Outside of auth tokens, adapters are largely stateless.
|
|
98
|
+
|
|
99
|
+
## Anti-Patterns
|
|
100
|
+
|
|
101
|
+
1. **Hardcoding IDs:** Never hardcode product or variant IDs; always fetch them through the services.
|
|
102
|
+
2. **Ignoring Capabilities:** Calling `auth.deleteAccount` without checking if it's supported will throw an error.
|
|
103
|
+
3. **Direct API calls:** **NEVER** use `fetch` to talk to the backend directly from outside `@thorprovider/adapters`. If the adapter doesn't expose the method you need, add it to `@thorprovider/adapters` first, then consume it from L5. This is non-negotiable — there are no exceptions, not even for Admin API operations.
|
|
104
|
+
|
|
105
|
+
## Next Steps
|
|
106
|
+
|
|
107
|
+
- [Reference: Capabilities System](capabilities-system.md)
|
|
108
|
+
- [How-to: Authentication](authentication.md)
|
|
109
|
+
- [How-to: Profile Management](profile-management.md)
|
|
110
|
+
|
|
111
|
+
## Type Safety
|
|
112
|
+
|
|
113
|
+
All services are fully typed with TypeScript:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
// TypeScript knows the return type
|
|
117
|
+
const products: Product[] = await commerce.getProducts();
|
|
118
|
+
|
|
119
|
+
// TypeScript validates input
|
|
120
|
+
await commerce.addToCart(cart.id!, [
|
|
121
|
+
{
|
|
122
|
+
merchandiseId: 'variant_123', // ✅ Required
|
|
123
|
+
quantity: 2, // ✅ Required
|
|
124
|
+
customAttribute: 'value', // ❌ Error: invalid property
|
|
125
|
+
},
|
|
126
|
+
]);
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Resources
|
|
130
|
+
|
|
131
|
+
- [Capabilities System](./capabilities-system.md) - Feature detection
|
|
132
|
+
- [Providers Overview](./providers-overview.md) - Backend support
|
|
133
|
+
- [Authentication](./authentication.md) - Auth details
|
|
134
|
+
- [Profile Management](./profile-management.md) - Customer operations
|
|
135
|
+
- [TypeScript Types](../src/types/) - Full type definitions
|