@thorprovider/adapters 5.0.7 → 5.0.9
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/dist/index.d.mts +43 -3
- package/dist/index.d.ts +43 -3
- package/dist/index.js +155 -34
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +155 -34
- package/dist/index.mjs.map +1 -1
- package/docs/multi-tenant-channel-scoping.md +42 -4
- package/package.json +2 -2
- package/src/core/errors.ts +2 -1
- package/src/providers/medusa/index.test.ts +40 -0
- package/src/providers/medusa/index.ts +173 -36
- package/src/providers/medusa/transforms.ts +21 -13
package/dist/index.mjs
CHANGED
|
@@ -373,12 +373,14 @@ var ResourceNotFoundError = class extends CommerceError {
|
|
|
373
373
|
}
|
|
374
374
|
};
|
|
375
375
|
var ProviderAPIError = class extends CommerceError {
|
|
376
|
-
constructor(message, provider, status) {
|
|
376
|
+
constructor(message, provider, status, details) {
|
|
377
377
|
super(message, "PROVIDER_API_ERROR", status);
|
|
378
378
|
this.provider = provider;
|
|
379
|
+
this.details = details;
|
|
379
380
|
this.name = "ProviderAPIError";
|
|
380
381
|
}
|
|
381
382
|
provider;
|
|
383
|
+
details;
|
|
382
384
|
};
|
|
383
385
|
function extractErrorMessage(error) {
|
|
384
386
|
if (error instanceof Error && error.message) {
|
|
@@ -1055,6 +1057,12 @@ function transformCartItem(item, cartCurrencyCode) {
|
|
|
1055
1057
|
const product = variant?.product;
|
|
1056
1058
|
const currencyCode = resolveMedusaCartItemCurrencyCode(item, cartCurrencyCode);
|
|
1057
1059
|
const totalCents = resolveMedusaCartItemTotal(item);
|
|
1060
|
+
const merchandiseId = variant?.id ?? item.variant_id ?? "";
|
|
1061
|
+
const merchandiseTitle = variant?.title ?? item.variant_title ?? "";
|
|
1062
|
+
const productId = product?.id ?? item.product_id ?? "";
|
|
1063
|
+
const productHandle = product?.handle ?? item.product_handle ?? "";
|
|
1064
|
+
const productTitle = product?.title ?? item.product_title ?? "";
|
|
1065
|
+
const thumbnail = product?.thumbnail ?? item.thumbnail ?? "";
|
|
1058
1066
|
return {
|
|
1059
1067
|
id: item.id,
|
|
1060
1068
|
quantity: item.quantity ?? 1,
|
|
@@ -1062,17 +1070,17 @@ function transformCartItem(item, cartCurrencyCode) {
|
|
|
1062
1070
|
totalAmount: transformMoney(totalCents, currencyCode)
|
|
1063
1071
|
},
|
|
1064
1072
|
merchandise: {
|
|
1065
|
-
id:
|
|
1066
|
-
title:
|
|
1073
|
+
id: merchandiseId,
|
|
1074
|
+
title: merchandiseTitle,
|
|
1067
1075
|
selectedOptions: variant?.options?.map((opt) => ({
|
|
1068
1076
|
name: opt.option?.title ?? "",
|
|
1069
1077
|
value: opt.value ?? ""
|
|
1070
1078
|
})) ?? [],
|
|
1071
1079
|
product: {
|
|
1072
|
-
id:
|
|
1073
|
-
handle:
|
|
1074
|
-
title:
|
|
1075
|
-
featuredImage:
|
|
1080
|
+
id: productId,
|
|
1081
|
+
handle: productHandle,
|
|
1082
|
+
title: productTitle,
|
|
1083
|
+
featuredImage: thumbnail ? transformImage(thumbnail, productTitle) : transformImage("", "")
|
|
1076
1084
|
}
|
|
1077
1085
|
}
|
|
1078
1086
|
};
|
|
@@ -2209,6 +2217,7 @@ var MedusaProvider = class {
|
|
|
2209
2217
|
retryConfig;
|
|
2210
2218
|
baseUrl;
|
|
2211
2219
|
publishableKey;
|
|
2220
|
+
regionId;
|
|
2212
2221
|
/** Secret API key for admin operations (if configured). */
|
|
2213
2222
|
adminApiKey;
|
|
2214
2223
|
/**
|
|
@@ -2252,6 +2261,7 @@ var MedusaProvider = class {
|
|
|
2252
2261
|
this.currencyCode = config.currencyCode ?? "USD";
|
|
2253
2262
|
this.baseUrl = config.baseUrl;
|
|
2254
2263
|
this.publishableKey = config.publishableKey;
|
|
2264
|
+
this.regionId = config.regionId;
|
|
2255
2265
|
this.retryConfig = {
|
|
2256
2266
|
maxRetries: config.retry?.maxRetries ?? 3,
|
|
2257
2267
|
timeoutMs: config.retry?.timeoutMs ?? 1e4,
|
|
@@ -2386,16 +2396,17 @@ var MedusaProvider = class {
|
|
|
2386
2396
|
try {
|
|
2387
2397
|
return await this.withRetry(
|
|
2388
2398
|
async () => {
|
|
2399
|
+
const regionId = options?.regionId ?? this.regionId;
|
|
2389
2400
|
const baseFields = "+variants,+variants.inventory_quantity,+options,+images,+tags";
|
|
2390
2401
|
const pricingFields = ",+variants.calculated_price,+variants.prices";
|
|
2391
|
-
const fields =
|
|
2402
|
+
const fields = regionId ? baseFields + pricingFields : baseFields;
|
|
2392
2403
|
const queryParams = {
|
|
2393
2404
|
handle,
|
|
2394
2405
|
limit: 1,
|
|
2395
2406
|
fields
|
|
2396
2407
|
};
|
|
2397
|
-
if (
|
|
2398
|
-
queryParams.region_id =
|
|
2408
|
+
if (regionId) {
|
|
2409
|
+
queryParams.region_id = regionId;
|
|
2399
2410
|
}
|
|
2400
2411
|
const salesChannelId = process.env.NEXT_PUBLIC_SALES_CHANNEL_ID;
|
|
2401
2412
|
if (salesChannelId) {
|
|
@@ -2425,9 +2436,10 @@ var MedusaProvider = class {
|
|
|
2425
2436
|
try {
|
|
2426
2437
|
return await this.withRetry(
|
|
2427
2438
|
async () => {
|
|
2439
|
+
const regionId = options.regionId ?? this.regionId;
|
|
2428
2440
|
const baseFields = "+variants,+variants.inventory_quantity,+options,+images,+tags";
|
|
2429
2441
|
const pricingFields = ",+variants.calculated_price,+variants.prices";
|
|
2430
|
-
const fields =
|
|
2442
|
+
const fields = regionId ? baseFields + pricingFields : baseFields;
|
|
2431
2443
|
const SORT_KEY_MAP = {
|
|
2432
2444
|
featured: null,
|
|
2433
2445
|
// server default order
|
|
@@ -2454,8 +2466,8 @@ var MedusaProvider = class {
|
|
|
2454
2466
|
queryParams.order = mappedOrder;
|
|
2455
2467
|
}
|
|
2456
2468
|
}
|
|
2457
|
-
if (
|
|
2458
|
-
queryParams.region_id =
|
|
2469
|
+
if (regionId) {
|
|
2470
|
+
queryParams.region_id = regionId;
|
|
2459
2471
|
}
|
|
2460
2472
|
if (options.categoryIds && options.categoryIds.length > 0) {
|
|
2461
2473
|
queryParams.category_id = options.categoryIds;
|
|
@@ -2848,6 +2860,11 @@ var MedusaProvider = class {
|
|
|
2848
2860
|
limit
|
|
2849
2861
|
});
|
|
2850
2862
|
let result = collections.map(transformCollection);
|
|
2863
|
+
const salesChannelId = options?.salesChannelId ?? process.env.NEXT_PUBLIC_SALES_CHANNEL_ID;
|
|
2864
|
+
const activeCollectionIds = await this._getChannelCollections(salesChannelId);
|
|
2865
|
+
if (activeCollectionIds) {
|
|
2866
|
+
result = result.filter((c) => activeCollectionIds.has(c.id));
|
|
2867
|
+
}
|
|
2851
2868
|
if (options?.query) {
|
|
2852
2869
|
const q = options.query.toLowerCase();
|
|
2853
2870
|
result = result.filter(
|
|
@@ -2864,6 +2881,89 @@ var MedusaProvider = class {
|
|
|
2864
2881
|
);
|
|
2865
2882
|
}
|
|
2866
2883
|
}
|
|
2884
|
+
/**
|
|
2885
|
+
* Returns the set of product-collection IDs that have at least one product
|
|
2886
|
+
* available in the given sales channel (legacy fallback).
|
|
2887
|
+
*
|
|
2888
|
+
* @deprecated Use `_getChannelCollections()` instead (backed by Thor Commerce plugin).
|
|
2889
|
+
*/
|
|
2890
|
+
async _getActiveCollectionIds(salesChannelId) {
|
|
2891
|
+
this.logger.debug("_getActiveCollectionIds", { salesChannelId });
|
|
2892
|
+
const collectionIds = /* @__PURE__ */ new Set();
|
|
2893
|
+
const limit = 500;
|
|
2894
|
+
let offset = 0;
|
|
2895
|
+
while (true) {
|
|
2896
|
+
const { products } = await this.sdk.store.product.list({
|
|
2897
|
+
sales_channel_id: [salesChannelId],
|
|
2898
|
+
fields: "+collections.id",
|
|
2899
|
+
limit,
|
|
2900
|
+
offset
|
|
2901
|
+
});
|
|
2902
|
+
for (const product of products ?? []) {
|
|
2903
|
+
for (const col of product.collections ?? []) {
|
|
2904
|
+
if (col?.id) collectionIds.add(col.id);
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
if (!products || products.length < limit) break;
|
|
2908
|
+
offset += limit;
|
|
2909
|
+
}
|
|
2910
|
+
this.logger.debug("_getActiveCollectionIds result", { count: collectionIds.size });
|
|
2911
|
+
return collectionIds;
|
|
2912
|
+
}
|
|
2913
|
+
/**
|
|
2914
|
+
* Fetch channel-scoped collections from Thor Commerce plugin.
|
|
2915
|
+
* Uses `GET /store/thor/collections` — the endpoint resolves the sales
|
|
2916
|
+
* channel from the publishable key header itself.
|
|
2917
|
+
*
|
|
2918
|
+
* Falls back to `_getActiveCollectionIds()` (legacy N+1) when the plugin is
|
|
2919
|
+
* not available; the legacy fallback needs an explicit `salesChannelId`.
|
|
2920
|
+
* Returns `null` when no scoping could be performed.
|
|
2921
|
+
*/
|
|
2922
|
+
async _getChannelCollections(salesChannelId) {
|
|
2923
|
+
try {
|
|
2924
|
+
const url = `${this.baseUrl}/store/thor/collections`;
|
|
2925
|
+
const response = await fetchWithRetry(
|
|
2926
|
+
url,
|
|
2927
|
+
{
|
|
2928
|
+
method: "GET",
|
|
2929
|
+
headers: {
|
|
2930
|
+
"x-publishable-api-key": this.publishableKey,
|
|
2931
|
+
"Content-Type": "application/json"
|
|
2932
|
+
}
|
|
2933
|
+
},
|
|
2934
|
+
this.retryConfig
|
|
2935
|
+
);
|
|
2936
|
+
if (response.status === 404 || !response.ok) {
|
|
2937
|
+
this.logger.debug("Thor plugin collections endpoint unavailable, falling back to legacy method");
|
|
2938
|
+
return this._legacyChannelCollections(salesChannelId);
|
|
2939
|
+
}
|
|
2940
|
+
const data = await response.json();
|
|
2941
|
+
const collectionIds = /* @__PURE__ */ new Set();
|
|
2942
|
+
for (const col of data.collections ?? []) {
|
|
2943
|
+
if (col?.id) collectionIds.add(col.id);
|
|
2944
|
+
}
|
|
2945
|
+
this.logger.debug("_getChannelCollections result (Thor plugin)", { count: collectionIds.size });
|
|
2946
|
+
return collectionIds;
|
|
2947
|
+
} catch (error) {
|
|
2948
|
+
this.logger.warn("Failed to fetch channel collections from Thor plugin, falling back to legacy method", {
|
|
2949
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2950
|
+
});
|
|
2951
|
+
return this._legacyChannelCollections(salesChannelId);
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
/**
|
|
2955
|
+
* Legacy fallback for `_getChannelCollections()` when the Thor plugin is not
|
|
2956
|
+
* installed. Requires an explicit `salesChannelId`; returns `null` otherwise.
|
|
2957
|
+
*/
|
|
2958
|
+
async _legacyChannelCollections(salesChannelId) {
|
|
2959
|
+
if (!salesChannelId) {
|
|
2960
|
+
this.logger.warn(
|
|
2961
|
+
"Cannot scope collections: Thor plugin unavailable and no sales channel id provided; returning unscoped collections"
|
|
2962
|
+
);
|
|
2963
|
+
return null;
|
|
2964
|
+
}
|
|
2965
|
+
return this._getActiveCollectionIds(salesChannelId);
|
|
2966
|
+
}
|
|
2867
2967
|
async getCollectionProducts(handle, options = {}) {
|
|
2868
2968
|
this.logger.debug("getCollectionProducts", { handle, options });
|
|
2869
2969
|
try {
|
|
@@ -2958,8 +3058,18 @@ var MedusaProvider = class {
|
|
|
2958
3058
|
}
|
|
2959
3059
|
/**
|
|
2960
3060
|
* Fetch channel-scoped categories from Thor Commerce plugin.
|
|
2961
|
-
* Uses `GET /store/thor/categories` — a single indexed query.
|
|
2962
|
-
*
|
|
3061
|
+
* Uses `GET /store/thor/categories` — a single indexed query. The endpoint
|
|
3062
|
+
* resolves the sales channel from the publishable key header itself, so no
|
|
3063
|
+
* explicit channel id is required when the plugin is installed.
|
|
3064
|
+
*
|
|
3065
|
+
* Falls back to `_getActiveCategoryIds()` (legacy N+1) when the plugin is
|
|
3066
|
+
* not available. The legacy fallback needs an explicit `salesChannelId`;
|
|
3067
|
+
* when none is provided and the plugin is unavailable, returns `null` to
|
|
3068
|
+
* signal that scoping is not possible and the caller should skip filtering.
|
|
3069
|
+
*
|
|
3070
|
+
* @param salesChannelId - Only used by the legacy fallback (plugin absent).
|
|
3071
|
+
* @returns Set of category IDs active in the channel, or `null` when no
|
|
3072
|
+
* scoping could be performed.
|
|
2963
3073
|
*/
|
|
2964
3074
|
async _getChannelCategories(salesChannelId) {
|
|
2965
3075
|
try {
|
|
@@ -2975,15 +3085,9 @@ var MedusaProvider = class {
|
|
|
2975
3085
|
},
|
|
2976
3086
|
this.retryConfig
|
|
2977
3087
|
);
|
|
2978
|
-
if (response.status === 404) {
|
|
2979
|
-
this.logger.debug("Thor plugin categories endpoint
|
|
2980
|
-
return this.
|
|
2981
|
-
}
|
|
2982
|
-
if (!response.ok) {
|
|
2983
|
-
this.logger.warn("Thor plugin categories endpoint returned non-OK status", {
|
|
2984
|
-
status: response.status
|
|
2985
|
-
});
|
|
2986
|
-
return this._getActiveCategoryIds(salesChannelId);
|
|
3088
|
+
if (response.status === 404 || !response.ok) {
|
|
3089
|
+
this.logger.debug("Thor plugin categories endpoint unavailable, falling back to legacy method");
|
|
3090
|
+
return this._legacyChannelCategories(salesChannelId);
|
|
2987
3091
|
}
|
|
2988
3092
|
const data = await response.json();
|
|
2989
3093
|
const categoryIds = /* @__PURE__ */ new Set();
|
|
@@ -2996,8 +3100,21 @@ var MedusaProvider = class {
|
|
|
2996
3100
|
this.logger.warn("Failed to fetch channel categories from Thor plugin, falling back to legacy method", {
|
|
2997
3101
|
error: error instanceof Error ? error.message : String(error)
|
|
2998
3102
|
});
|
|
2999
|
-
return this.
|
|
3103
|
+
return this._legacyChannelCategories(salesChannelId);
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
/**
|
|
3107
|
+
* Legacy fallback for `_getChannelCategories()` when the Thor plugin is not
|
|
3108
|
+
* installed. Requires an explicit `salesChannelId`; returns `null` otherwise.
|
|
3109
|
+
*/
|
|
3110
|
+
async _legacyChannelCategories(salesChannelId) {
|
|
3111
|
+
if (!salesChannelId) {
|
|
3112
|
+
this.logger.warn(
|
|
3113
|
+
"Cannot scope categories: Thor plugin unavailable and no sales channel id provided; returning unscoped categories"
|
|
3114
|
+
);
|
|
3115
|
+
return null;
|
|
3000
3116
|
}
|
|
3117
|
+
return this._getActiveCategoryIds(salesChannelId);
|
|
3001
3118
|
}
|
|
3002
3119
|
async getCategories(options = {}) {
|
|
3003
3120
|
this.logger.debug("getCategories", { options });
|
|
@@ -3018,8 +3135,8 @@ var MedusaProvider = class {
|
|
|
3018
3135
|
const { product_categories } = await this.sdk.store.category.list(queryParams);
|
|
3019
3136
|
let result = product_categories.map(transformCategory);
|
|
3020
3137
|
const salesChannelId = options.salesChannelId ?? process.env.NEXT_PUBLIC_SALES_CHANNEL_ID;
|
|
3021
|
-
|
|
3022
|
-
|
|
3138
|
+
const activeCategoryIds = await this._getChannelCategories(salesChannelId);
|
|
3139
|
+
if (activeCategoryIds) {
|
|
3023
3140
|
result = result.filter((c) => activeCategoryIds.has(c.id));
|
|
3024
3141
|
}
|
|
3025
3142
|
if (options.query) {
|
|
@@ -3052,6 +3169,9 @@ var MedusaProvider = class {
|
|
|
3052
3169
|
offset: options.after ? parseInt(options.after, 10) : void 0,
|
|
3053
3170
|
fields: "+variants,+variants.prices,+variants.calculated_prices,+variants.inventory_quantity,+options,+images,+tags"
|
|
3054
3171
|
};
|
|
3172
|
+
if (this.regionId) {
|
|
3173
|
+
categoryQueryParams.region_id = this.regionId;
|
|
3174
|
+
}
|
|
3055
3175
|
const salesChannelId = process.env.NEXT_PUBLIC_SALES_CHANNEL_ID;
|
|
3056
3176
|
if (salesChannelId) {
|
|
3057
3177
|
categoryQueryParams.sales_channel_id = [salesChannelId];
|
|
@@ -3072,7 +3192,10 @@ var MedusaProvider = class {
|
|
|
3072
3192
|
// Cart Operations
|
|
3073
3193
|
// ============================================
|
|
3074
3194
|
async createCart(options) {
|
|
3075
|
-
this.logger.debug("createCart", {
|
|
3195
|
+
this.logger.debug("createCart", {
|
|
3196
|
+
regionId: options?.regionId,
|
|
3197
|
+
salesChannelId: options?.salesChannelId
|
|
3198
|
+
});
|
|
3076
3199
|
try {
|
|
3077
3200
|
const createPayload = {};
|
|
3078
3201
|
if (options?.regionId) {
|
|
@@ -3080,6 +3203,10 @@ var MedusaProvider = class {
|
|
|
3080
3203
|
} else {
|
|
3081
3204
|
createPayload.currency_code = this.currencyCode.toLowerCase();
|
|
3082
3205
|
}
|
|
3206
|
+
const salesChannelId = options?.salesChannelId ?? process.env.NEXT_PUBLIC_SALES_CHANNEL_ID;
|
|
3207
|
+
if (salesChannelId) {
|
|
3208
|
+
createPayload.sales_channel_id = salesChannelId;
|
|
3209
|
+
}
|
|
3083
3210
|
const { cart } = await this.sdk.store.cart.create(createPayload);
|
|
3084
3211
|
return transformCart(cart);
|
|
3085
3212
|
} catch (error) {
|
|
@@ -3121,13 +3248,11 @@ var MedusaProvider = class {
|
|
|
3121
3248
|
"retrieveCartBeforeAdd",
|
|
3122
3249
|
retrieveCtx
|
|
3123
3250
|
);
|
|
3124
|
-
console.log("retrieveResult =", JSON.stringify(retrieveResult, null, 2));
|
|
3125
3251
|
const backendCart = retrieveResult?.cart;
|
|
3126
3252
|
if (!backendCart) {
|
|
3127
3253
|
this.logger.warn("Cart not found before addToCart", { cartId });
|
|
3128
3254
|
throw new ProviderAPIError("Cart not found. Please refresh and try again.", "medusa", 404);
|
|
3129
3255
|
}
|
|
3130
|
-
console.log("backendCart =", JSON.stringify(backendCart, null, 2));
|
|
3131
3256
|
if (!backendCart.region_id) {
|
|
3132
3257
|
this.logger.warn("Attempted to add item to cart with no region_id", { cartId });
|
|
3133
3258
|
throw new ProviderAPIError(
|
|
@@ -3176,15 +3301,11 @@ var MedusaProvider = class {
|
|
|
3176
3301
|
404
|
|
3177
3302
|
);
|
|
3178
3303
|
}
|
|
3179
|
-
console.log("Found Variant:", JSON.stringify(foundVariant, null, 2));
|
|
3180
3304
|
const variant = foundVariant;
|
|
3181
3305
|
let hasPriceForRegion = false;
|
|
3182
3306
|
if (variant?.calculated_price && (variant.calculated_price.calculated_amount ?? null) !== null) {
|
|
3183
3307
|
const calcCurrency = (variant.calculated_price.currency_code || "").toLowerCase();
|
|
3184
3308
|
const cartCurrency = (backendCart.currency_code || this.currencyCode || "").toLowerCase();
|
|
3185
|
-
console.log("calcCurrency =", calcCurrency);
|
|
3186
|
-
console.log("cartCurrency =", cartCurrency);
|
|
3187
|
-
console.log("this.currencyCode =", this.currencyCode);
|
|
3188
3309
|
if (!cartCurrency || calcCurrency === cartCurrency || calcCurrency === this.currencyCode.toLowerCase()) {
|
|
3189
3310
|
hasPriceForRegion = true;
|
|
3190
3311
|
}
|