@thorprovider/adapters 5.0.8 → 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 +41 -2
- package/dist/index.d.ts +41 -2
- package/dist/index.js +152 -33
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +152 -33
- package/dist/index.mjs.map +1 -1
- package/docs/multi-tenant-channel-scoping.md +42 -4
- package/package.json +2 -2
- 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
|
@@ -1057,6 +1057,12 @@ function transformCartItem(item, cartCurrencyCode) {
|
|
|
1057
1057
|
const product = variant?.product;
|
|
1058
1058
|
const currencyCode = resolveMedusaCartItemCurrencyCode(item, cartCurrencyCode);
|
|
1059
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 ?? "";
|
|
1060
1066
|
return {
|
|
1061
1067
|
id: item.id,
|
|
1062
1068
|
quantity: item.quantity ?? 1,
|
|
@@ -1064,17 +1070,17 @@ function transformCartItem(item, cartCurrencyCode) {
|
|
|
1064
1070
|
totalAmount: transformMoney(totalCents, currencyCode)
|
|
1065
1071
|
},
|
|
1066
1072
|
merchandise: {
|
|
1067
|
-
id:
|
|
1068
|
-
title:
|
|
1073
|
+
id: merchandiseId,
|
|
1074
|
+
title: merchandiseTitle,
|
|
1069
1075
|
selectedOptions: variant?.options?.map((opt) => ({
|
|
1070
1076
|
name: opt.option?.title ?? "",
|
|
1071
1077
|
value: opt.value ?? ""
|
|
1072
1078
|
})) ?? [],
|
|
1073
1079
|
product: {
|
|
1074
|
-
id:
|
|
1075
|
-
handle:
|
|
1076
|
-
title:
|
|
1077
|
-
featuredImage:
|
|
1080
|
+
id: productId,
|
|
1081
|
+
handle: productHandle,
|
|
1082
|
+
title: productTitle,
|
|
1083
|
+
featuredImage: thumbnail ? transformImage(thumbnail, productTitle) : transformImage("", "")
|
|
1078
1084
|
}
|
|
1079
1085
|
}
|
|
1080
1086
|
};
|
|
@@ -2211,6 +2217,7 @@ var MedusaProvider = class {
|
|
|
2211
2217
|
retryConfig;
|
|
2212
2218
|
baseUrl;
|
|
2213
2219
|
publishableKey;
|
|
2220
|
+
regionId;
|
|
2214
2221
|
/** Secret API key for admin operations (if configured). */
|
|
2215
2222
|
adminApiKey;
|
|
2216
2223
|
/**
|
|
@@ -2254,6 +2261,7 @@ var MedusaProvider = class {
|
|
|
2254
2261
|
this.currencyCode = config.currencyCode ?? "USD";
|
|
2255
2262
|
this.baseUrl = config.baseUrl;
|
|
2256
2263
|
this.publishableKey = config.publishableKey;
|
|
2264
|
+
this.regionId = config.regionId;
|
|
2257
2265
|
this.retryConfig = {
|
|
2258
2266
|
maxRetries: config.retry?.maxRetries ?? 3,
|
|
2259
2267
|
timeoutMs: config.retry?.timeoutMs ?? 1e4,
|
|
@@ -2388,16 +2396,17 @@ var MedusaProvider = class {
|
|
|
2388
2396
|
try {
|
|
2389
2397
|
return await this.withRetry(
|
|
2390
2398
|
async () => {
|
|
2399
|
+
const regionId = options?.regionId ?? this.regionId;
|
|
2391
2400
|
const baseFields = "+variants,+variants.inventory_quantity,+options,+images,+tags";
|
|
2392
2401
|
const pricingFields = ",+variants.calculated_price,+variants.prices";
|
|
2393
|
-
const fields =
|
|
2402
|
+
const fields = regionId ? baseFields + pricingFields : baseFields;
|
|
2394
2403
|
const queryParams = {
|
|
2395
2404
|
handle,
|
|
2396
2405
|
limit: 1,
|
|
2397
2406
|
fields
|
|
2398
2407
|
};
|
|
2399
|
-
if (
|
|
2400
|
-
queryParams.region_id =
|
|
2408
|
+
if (regionId) {
|
|
2409
|
+
queryParams.region_id = regionId;
|
|
2401
2410
|
}
|
|
2402
2411
|
const salesChannelId = process.env.NEXT_PUBLIC_SALES_CHANNEL_ID;
|
|
2403
2412
|
if (salesChannelId) {
|
|
@@ -2427,9 +2436,10 @@ var MedusaProvider = class {
|
|
|
2427
2436
|
try {
|
|
2428
2437
|
return await this.withRetry(
|
|
2429
2438
|
async () => {
|
|
2439
|
+
const regionId = options.regionId ?? this.regionId;
|
|
2430
2440
|
const baseFields = "+variants,+variants.inventory_quantity,+options,+images,+tags";
|
|
2431
2441
|
const pricingFields = ",+variants.calculated_price,+variants.prices";
|
|
2432
|
-
const fields =
|
|
2442
|
+
const fields = regionId ? baseFields + pricingFields : baseFields;
|
|
2433
2443
|
const SORT_KEY_MAP = {
|
|
2434
2444
|
featured: null,
|
|
2435
2445
|
// server default order
|
|
@@ -2456,8 +2466,8 @@ var MedusaProvider = class {
|
|
|
2456
2466
|
queryParams.order = mappedOrder;
|
|
2457
2467
|
}
|
|
2458
2468
|
}
|
|
2459
|
-
if (
|
|
2460
|
-
queryParams.region_id =
|
|
2469
|
+
if (regionId) {
|
|
2470
|
+
queryParams.region_id = regionId;
|
|
2461
2471
|
}
|
|
2462
2472
|
if (options.categoryIds && options.categoryIds.length > 0) {
|
|
2463
2473
|
queryParams.category_id = options.categoryIds;
|
|
@@ -2850,6 +2860,11 @@ var MedusaProvider = class {
|
|
|
2850
2860
|
limit
|
|
2851
2861
|
});
|
|
2852
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
|
+
}
|
|
2853
2868
|
if (options?.query) {
|
|
2854
2869
|
const q = options.query.toLowerCase();
|
|
2855
2870
|
result = result.filter(
|
|
@@ -2866,6 +2881,89 @@ var MedusaProvider = class {
|
|
|
2866
2881
|
);
|
|
2867
2882
|
}
|
|
2868
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
|
+
}
|
|
2869
2967
|
async getCollectionProducts(handle, options = {}) {
|
|
2870
2968
|
this.logger.debug("getCollectionProducts", { handle, options });
|
|
2871
2969
|
try {
|
|
@@ -2960,8 +3058,18 @@ var MedusaProvider = class {
|
|
|
2960
3058
|
}
|
|
2961
3059
|
/**
|
|
2962
3060
|
* Fetch channel-scoped categories from Thor Commerce plugin.
|
|
2963
|
-
* Uses `GET /store/thor/categories` — a single indexed query.
|
|
2964
|
-
*
|
|
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.
|
|
2965
3073
|
*/
|
|
2966
3074
|
async _getChannelCategories(salesChannelId) {
|
|
2967
3075
|
try {
|
|
@@ -2977,15 +3085,9 @@ var MedusaProvider = class {
|
|
|
2977
3085
|
},
|
|
2978
3086
|
this.retryConfig
|
|
2979
3087
|
);
|
|
2980
|
-
if (response.status === 404) {
|
|
2981
|
-
this.logger.debug("Thor plugin categories endpoint
|
|
2982
|
-
return this.
|
|
2983
|
-
}
|
|
2984
|
-
if (!response.ok) {
|
|
2985
|
-
this.logger.warn("Thor plugin categories endpoint returned non-OK status", {
|
|
2986
|
-
status: response.status
|
|
2987
|
-
});
|
|
2988
|
-
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);
|
|
2989
3091
|
}
|
|
2990
3092
|
const data = await response.json();
|
|
2991
3093
|
const categoryIds = /* @__PURE__ */ new Set();
|
|
@@ -2998,8 +3100,21 @@ var MedusaProvider = class {
|
|
|
2998
3100
|
this.logger.warn("Failed to fetch channel categories from Thor plugin, falling back to legacy method", {
|
|
2999
3101
|
error: error instanceof Error ? error.message : String(error)
|
|
3000
3102
|
});
|
|
3001
|
-
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;
|
|
3002
3116
|
}
|
|
3117
|
+
return this._getActiveCategoryIds(salesChannelId);
|
|
3003
3118
|
}
|
|
3004
3119
|
async getCategories(options = {}) {
|
|
3005
3120
|
this.logger.debug("getCategories", { options });
|
|
@@ -3020,8 +3135,8 @@ var MedusaProvider = class {
|
|
|
3020
3135
|
const { product_categories } = await this.sdk.store.category.list(queryParams);
|
|
3021
3136
|
let result = product_categories.map(transformCategory);
|
|
3022
3137
|
const salesChannelId = options.salesChannelId ?? process.env.NEXT_PUBLIC_SALES_CHANNEL_ID;
|
|
3023
|
-
|
|
3024
|
-
|
|
3138
|
+
const activeCategoryIds = await this._getChannelCategories(salesChannelId);
|
|
3139
|
+
if (activeCategoryIds) {
|
|
3025
3140
|
result = result.filter((c) => activeCategoryIds.has(c.id));
|
|
3026
3141
|
}
|
|
3027
3142
|
if (options.query) {
|
|
@@ -3054,6 +3169,9 @@ var MedusaProvider = class {
|
|
|
3054
3169
|
offset: options.after ? parseInt(options.after, 10) : void 0,
|
|
3055
3170
|
fields: "+variants,+variants.prices,+variants.calculated_prices,+variants.inventory_quantity,+options,+images,+tags"
|
|
3056
3171
|
};
|
|
3172
|
+
if (this.regionId) {
|
|
3173
|
+
categoryQueryParams.region_id = this.regionId;
|
|
3174
|
+
}
|
|
3057
3175
|
const salesChannelId = process.env.NEXT_PUBLIC_SALES_CHANNEL_ID;
|
|
3058
3176
|
if (salesChannelId) {
|
|
3059
3177
|
categoryQueryParams.sales_channel_id = [salesChannelId];
|
|
@@ -3074,7 +3192,10 @@ var MedusaProvider = class {
|
|
|
3074
3192
|
// Cart Operations
|
|
3075
3193
|
// ============================================
|
|
3076
3194
|
async createCart(options) {
|
|
3077
|
-
this.logger.debug("createCart", {
|
|
3195
|
+
this.logger.debug("createCart", {
|
|
3196
|
+
regionId: options?.regionId,
|
|
3197
|
+
salesChannelId: options?.salesChannelId
|
|
3198
|
+
});
|
|
3078
3199
|
try {
|
|
3079
3200
|
const createPayload = {};
|
|
3080
3201
|
if (options?.regionId) {
|
|
@@ -3082,6 +3203,10 @@ var MedusaProvider = class {
|
|
|
3082
3203
|
} else {
|
|
3083
3204
|
createPayload.currency_code = this.currencyCode.toLowerCase();
|
|
3084
3205
|
}
|
|
3206
|
+
const salesChannelId = options?.salesChannelId ?? process.env.NEXT_PUBLIC_SALES_CHANNEL_ID;
|
|
3207
|
+
if (salesChannelId) {
|
|
3208
|
+
createPayload.sales_channel_id = salesChannelId;
|
|
3209
|
+
}
|
|
3085
3210
|
const { cart } = await this.sdk.store.cart.create(createPayload);
|
|
3086
3211
|
return transformCart(cart);
|
|
3087
3212
|
} catch (error) {
|
|
@@ -3123,13 +3248,11 @@ var MedusaProvider = class {
|
|
|
3123
3248
|
"retrieveCartBeforeAdd",
|
|
3124
3249
|
retrieveCtx
|
|
3125
3250
|
);
|
|
3126
|
-
console.log("retrieveResult =", JSON.stringify(retrieveResult, null, 2));
|
|
3127
3251
|
const backendCart = retrieveResult?.cart;
|
|
3128
3252
|
if (!backendCart) {
|
|
3129
3253
|
this.logger.warn("Cart not found before addToCart", { cartId });
|
|
3130
3254
|
throw new ProviderAPIError("Cart not found. Please refresh and try again.", "medusa", 404);
|
|
3131
3255
|
}
|
|
3132
|
-
console.log("backendCart =", JSON.stringify(backendCart, null, 2));
|
|
3133
3256
|
if (!backendCart.region_id) {
|
|
3134
3257
|
this.logger.warn("Attempted to add item to cart with no region_id", { cartId });
|
|
3135
3258
|
throw new ProviderAPIError(
|
|
@@ -3178,15 +3301,11 @@ var MedusaProvider = class {
|
|
|
3178
3301
|
404
|
|
3179
3302
|
);
|
|
3180
3303
|
}
|
|
3181
|
-
console.log("Found Variant:", JSON.stringify(foundVariant, null, 2));
|
|
3182
3304
|
const variant = foundVariant;
|
|
3183
3305
|
let hasPriceForRegion = false;
|
|
3184
3306
|
if (variant?.calculated_price && (variant.calculated_price.calculated_amount ?? null) !== null) {
|
|
3185
3307
|
const calcCurrency = (variant.calculated_price.currency_code || "").toLowerCase();
|
|
3186
3308
|
const cartCurrency = (backendCart.currency_code || this.currencyCode || "").toLowerCase();
|
|
3187
|
-
console.log("calcCurrency =", calcCurrency);
|
|
3188
|
-
console.log("cartCurrency =", cartCurrency);
|
|
3189
|
-
console.log("this.currencyCode =", this.currencyCode);
|
|
3190
3309
|
if (!cartCurrency || calcCurrency === cartCurrency || calcCurrency === this.currencyCode.toLowerCase()) {
|
|
3191
3310
|
hasPriceForRegion = true;
|
|
3192
3311
|
}
|