@reactionary/feeds 0.0.1
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/README.md +257 -0
- package/bin/reactionary-feeds +2 -0
- package/cli.cjs +170941 -0
- package/cli.js +340 -0
- package/documentation/basic-product-feeds.config.mjs +32 -0
- package/documentation/multilingual-feeds.config.mjs +59 -0
- package/documentation/nextjs-route-example.mjs +31 -0
- package/documentation/sitemaps.config.mjs +77 -0
- package/index.js +13 -0
- package/lib/default-registry.js +18 -0
- package/lib/env-client-builder.js +244 -0
- package/lib/feed-generator.js +395 -0
- package/lib/feed-server.js +305 -0
- package/lib/feed-transformer.js +21 -0
- package/lib/feed-types.js +45 -0
- package/lib/load-cwd-env.js +16 -0
- package/lib/sitemap-generator.js +173 -0
- package/lib/sitemap-xml.js +32 -0
- package/lib/transformers/acp-product-feed.transformer.js +128 -0
- package/lib/transformers/google-merchant-feed.transformer.js +60 -0
- package/lib/transformers/pricerunner-feed.transformer.js +60 -0
- package/lib/transformers/shared.js +25 -0
- package/lib/transformers/sitemap-feed.transformer.js +26 -0
- package/package.json +32 -0
- package/src/cli.d.ts +2 -0
- package/src/index.d.ts +13 -0
- package/src/lib/default-registry.d.ts +2 -0
- package/src/lib/env-client-builder.d.ts +18 -0
- package/src/lib/feed-generator.d.ts +17 -0
- package/src/lib/feed-server.d.ts +28 -0
- package/src/lib/feed-transformer.d.ts +30 -0
- package/src/lib/feed-types.d.ts +185 -0
- package/src/lib/load-cwd-env.d.ts +4 -0
- package/src/lib/sitemap-generator.d.ts +20 -0
- package/src/lib/sitemap-xml.d.ts +3 -0
- package/src/lib/transformers/acp-product-feed.transformer.d.ts +4 -0
- package/src/lib/transformers/google-merchant-feed.transformer.d.ts +2 -0
- package/src/lib/transformers/pricerunner-feed.transformer.d.ts +2 -0
- package/src/lib/transformers/shared.d.ts +6 -0
- package/src/lib/transformers/sitemap-feed.transformer.d.ts +2 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { withAlgoliaCapabilities } from "@reactionary/algolia";
|
|
2
|
+
import {
|
|
3
|
+
withCommercetoolsCapabilities
|
|
4
|
+
} from "@reactionary/commercetools";
|
|
5
|
+
import {
|
|
6
|
+
ClientBuilder,
|
|
7
|
+
MemoryCache,
|
|
8
|
+
createInitialRequestContext
|
|
9
|
+
} from "@reactionary/core";
|
|
10
|
+
import { withFakeCapabilities } from "@reactionary/fake";
|
|
11
|
+
import { withMagentoCapabilities } from "@reactionary/magento";
|
|
12
|
+
import { withMedusaCapabilities } from "@reactionary/medusa";
|
|
13
|
+
import {
|
|
14
|
+
withMeilisearchCapabilities
|
|
15
|
+
} from "@reactionary/meilisearch";
|
|
16
|
+
import { loadCwdEnv } from "./load-cwd-env.js";
|
|
17
|
+
const providerSystems = [
|
|
18
|
+
"FAKE",
|
|
19
|
+
"MAGENTO",
|
|
20
|
+
"MEDUSA",
|
|
21
|
+
"COMMERCETOOLS",
|
|
22
|
+
"ALGOLIA",
|
|
23
|
+
"MEILISEARCH",
|
|
24
|
+
"UNOMI"
|
|
25
|
+
];
|
|
26
|
+
function createReactionaryFeedClientFromEnv(options = {}) {
|
|
27
|
+
if (options.loadEnv !== false) {
|
|
28
|
+
loadCwdEnv({
|
|
29
|
+
env: options.env
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
const env = options.env ?? process.env;
|
|
33
|
+
const enabledSystems = getEnabledReactionaryFeedProviderSystems(env);
|
|
34
|
+
if (enabledSystems.length === 0) {
|
|
35
|
+
throw new Error(getNoEnabledProviderSystemsMessage());
|
|
36
|
+
}
|
|
37
|
+
const context = {
|
|
38
|
+
...createInitialRequestContext(),
|
|
39
|
+
...options.contextOverrides
|
|
40
|
+
};
|
|
41
|
+
let builder = new ClientBuilder(context).withCache(
|
|
42
|
+
options.cache ?? new MemoryCache()
|
|
43
|
+
);
|
|
44
|
+
for (const system of enabledSystems) {
|
|
45
|
+
switch (system) {
|
|
46
|
+
case "ALGOLIA":
|
|
47
|
+
builder = builder.withCapability(
|
|
48
|
+
withAlgoliaCapabilities(getAlgoliaConfiguration(env), {
|
|
49
|
+
productSearch: { enabled: true }
|
|
50
|
+
})
|
|
51
|
+
);
|
|
52
|
+
break;
|
|
53
|
+
case "COMMERCETOOLS":
|
|
54
|
+
builder = builder.withCapability(
|
|
55
|
+
withCommercetoolsCapabilities(getCommercetoolsConfiguration(env), {
|
|
56
|
+
product: { enabled: true },
|
|
57
|
+
category: { enabled: true },
|
|
58
|
+
inventory: { enabled: true },
|
|
59
|
+
price: { enabled: true },
|
|
60
|
+
productSearch: { enabled: true },
|
|
61
|
+
productReviews: { enabled: true },
|
|
62
|
+
store: { enabled: true }
|
|
63
|
+
})
|
|
64
|
+
);
|
|
65
|
+
break;
|
|
66
|
+
case "FAKE":
|
|
67
|
+
builder = builder.withCapability(
|
|
68
|
+
withFakeCapabilities(getFakeConfiguration(), {
|
|
69
|
+
productSearch: { enabled: true },
|
|
70
|
+
category: { enabled: true },
|
|
71
|
+
store: { enabled: true },
|
|
72
|
+
price: { enabled: true },
|
|
73
|
+
inventory: { enabled: true },
|
|
74
|
+
product: { enabled: true },
|
|
75
|
+
productReviews: { enabled: true }
|
|
76
|
+
})
|
|
77
|
+
);
|
|
78
|
+
break;
|
|
79
|
+
case "MAGENTO":
|
|
80
|
+
builder = builder.withCapability(
|
|
81
|
+
withMagentoCapabilities(getMagentoConfiguration(env), {
|
|
82
|
+
product: { enabled: true },
|
|
83
|
+
category: { enabled: true },
|
|
84
|
+
inventory: { enabled: true },
|
|
85
|
+
price: { enabled: true },
|
|
86
|
+
productSearch: { enabled: true },
|
|
87
|
+
productReviews: { enabled: true }
|
|
88
|
+
})
|
|
89
|
+
);
|
|
90
|
+
break;
|
|
91
|
+
case "MEDUSA":
|
|
92
|
+
builder = builder.withCapability(
|
|
93
|
+
withMedusaCapabilities(getMedusaConfiguration(env), {
|
|
94
|
+
product: { enabled: true },
|
|
95
|
+
category: { enabled: true },
|
|
96
|
+
inventory: { enabled: true },
|
|
97
|
+
price: { enabled: true },
|
|
98
|
+
productSearch: { enabled: true }
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
break;
|
|
102
|
+
case "MEILISEARCH":
|
|
103
|
+
builder = builder.withCapability(
|
|
104
|
+
withMeilisearchCapabilities(getMeilisearchConfiguration(env), {
|
|
105
|
+
productSearch: { enabled: true }
|
|
106
|
+
})
|
|
107
|
+
);
|
|
108
|
+
break;
|
|
109
|
+
case "UNOMI":
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
client: builder.build(),
|
|
115
|
+
enabledSystems
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function createReactionaryFeedClientFactoryFromEnv(options = {}) {
|
|
119
|
+
return (requestContext) => createReactionaryFeedClientFromEnv({
|
|
120
|
+
...options,
|
|
121
|
+
contextOverrides: requestContext
|
|
122
|
+
}).client;
|
|
123
|
+
}
|
|
124
|
+
function getEnabledReactionaryFeedProviderSystems(env = process.env) {
|
|
125
|
+
return providerSystems.filter(
|
|
126
|
+
(system) => isEnvEnabled(env[`ENABLED_${system}`])
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
function getNoEnabledProviderSystemsMessage() {
|
|
130
|
+
return `No Reactionary provider system is enabled. Set one of ${providerSystems.map((system) => `ENABLED_${system}=true`).join(", ")}.`;
|
|
131
|
+
}
|
|
132
|
+
function parseCommaSeparatedEnvList(value) {
|
|
133
|
+
return (value || "").split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
134
|
+
}
|
|
135
|
+
function isEnvEnabled(value) {
|
|
136
|
+
return value?.toLowerCase() === "true";
|
|
137
|
+
}
|
|
138
|
+
function getAlgoliaConfiguration(env) {
|
|
139
|
+
return {
|
|
140
|
+
apiKey: env["ALGOLIA_API_KEY"] || "",
|
|
141
|
+
appId: env["ALGOLIA_APP_ID"] || "",
|
|
142
|
+
indexName: env["ALGOLIA_INDEX"] || "",
|
|
143
|
+
useRecommendationsForBots: env["ALGOLIA_USE_RECOMMENDATIONS_FOR_BOTS"] === "true",
|
|
144
|
+
useBaseIndexNameForEnglishLocale: env["ALGOLIA_USE_BASE_INDEX_NAME_FOR_ENGLISH_LOCALE"] === "true"
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function getCommercetoolsConfiguration(env) {
|
|
148
|
+
return {
|
|
149
|
+
apiUrl: env["CTP_API_URL"] || "",
|
|
150
|
+
authUrl: env["CTP_AUTH_URL"] || "",
|
|
151
|
+
clientId: env["CTP_CLIENT_ID"] || "",
|
|
152
|
+
clientSecret: env["CTP_CLIENT_SECRET"] || "",
|
|
153
|
+
projectKey: env["CTP_PROJECT_KEY"] || "",
|
|
154
|
+
scopes: parseCommaSeparatedEnvList(env["CTP_SCOPES"]),
|
|
155
|
+
paymentMethods: [
|
|
156
|
+
{
|
|
157
|
+
identifier: {
|
|
158
|
+
method: "stripe",
|
|
159
|
+
name: "Stripe",
|
|
160
|
+
paymentProcessor: "stripe"
|
|
161
|
+
},
|
|
162
|
+
isPunchOut: false,
|
|
163
|
+
description: "Stripe payment gateway"
|
|
164
|
+
}
|
|
165
|
+
],
|
|
166
|
+
facetFieldsForSearch: parseCommaSeparatedEnvList(
|
|
167
|
+
env["CTP_FACET_FIELDS_FOR_SEARCH"]
|
|
168
|
+
),
|
|
169
|
+
adminClientId: env["CTP_ADMIN_CLIENT_ID"] || void 0,
|
|
170
|
+
adminClientSecret: env["CTP_ADMIN_CLIENT_SECRET"] || void 0,
|
|
171
|
+
listPriceChannelKey: env["CTP_LIST_PRICE_CHANNEL_KEY"] || void 0,
|
|
172
|
+
customerPriceChannelKey: env["CTP_CUSTOMER_PRICE_CHANNEL_KEY"] || void 0
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function getFakeConfiguration() {
|
|
176
|
+
return {
|
|
177
|
+
jitter: {
|
|
178
|
+
mean: 0,
|
|
179
|
+
deviation: 0
|
|
180
|
+
},
|
|
181
|
+
seeds: {
|
|
182
|
+
product: 1,
|
|
183
|
+
search: 1,
|
|
184
|
+
category: 1
|
|
185
|
+
},
|
|
186
|
+
featureFlags: {
|
|
187
|
+
flags: [
|
|
188
|
+
{
|
|
189
|
+
key: "true-flag",
|
|
190
|
+
type: "boolean",
|
|
191
|
+
enabled: true
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
key: "string-flag",
|
|
195
|
+
type: "multivariate",
|
|
196
|
+
variants: ["red", "green", "blue"],
|
|
197
|
+
enabledVariant: "blue"
|
|
198
|
+
}
|
|
199
|
+
]
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function getMagentoConfiguration(env) {
|
|
204
|
+
return {
|
|
205
|
+
adminApiKey: env["MAGENTO_ADMIN_API_KEY"] || "",
|
|
206
|
+
baseUrl: env["MAGENTO_BASE_URL"] || "",
|
|
207
|
+
storeCode: env["MAGENTO_STORE_CODE"] || "",
|
|
208
|
+
authStoreCode: env["MAGENTO_AUTH_STORE_CODE"] || "default",
|
|
209
|
+
mediaUrl: env["MAGENTO_MEDIA_URL"] || void 0,
|
|
210
|
+
mediaSource: env["MAGENTO_MEDIA_SOURCE"] === "EXTERNAL" ? "EXTERNAL" : "DEFAULT",
|
|
211
|
+
defaultCurrency: env["MAGENTO_DEFAULT_CURRENCY"] || "",
|
|
212
|
+
rootCategoryId: env["MAGENTO_ROOT_CATEGORY_ID"] || "2",
|
|
213
|
+
graphqlUrl: env["MAGENTO_GRAPHQL_URL"] || void 0,
|
|
214
|
+
reviewRatingCode: env["MAGENTO_REVIEW_RATING_CODE"] || void 0,
|
|
215
|
+
allCurrencies: []
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function getMedusaConfiguration(env) {
|
|
219
|
+
return {
|
|
220
|
+
publishable_key: env["MEDUSA_PUBLISHABLE_KEY"] || "",
|
|
221
|
+
adminApiKey: env["MEDUSA_ADMIN_KEY"] || "",
|
|
222
|
+
apiUrl: env["MEDUSA_API_URL"] || "",
|
|
223
|
+
defaultCurrency: env["MEDUSA_DEFAULT_CURRENCY"] || "",
|
|
224
|
+
allCurrencies: []
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function getMeilisearchConfiguration(env) {
|
|
228
|
+
return {
|
|
229
|
+
apiKey: env["MEILISEARCH_API_KEY"] || "",
|
|
230
|
+
apiUrl: env["MEILISEARCH_API_URL"] || "",
|
|
231
|
+
indexName: env["MEILISEARCH_INDEX"] || "",
|
|
232
|
+
useAIEmbedding: env["MEILISEARCH_USE_AI_EMBEDDING"] || void 0,
|
|
233
|
+
semanticRatio: env["MEILISEARCH_SEMANTIC_RATIO"] ? parseFloat(env["MEILISEARCH_SEMANTIC_RATIO"]) : 0.5,
|
|
234
|
+
orderIndexName: env["MEILISEARCH_ORDER_INDEX"] || "order",
|
|
235
|
+
useRecommendationsForBots: env["MEILISEARCH_USE_RECOMMENDATIONS_FOR_BOTS"] === "true"
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
export {
|
|
239
|
+
createReactionaryFeedClientFactoryFromEnv,
|
|
240
|
+
createReactionaryFeedClientFromEnv,
|
|
241
|
+
getEnabledReactionaryFeedProviderSystems,
|
|
242
|
+
getNoEnabledProviderSystemsMessage,
|
|
243
|
+
parseCommaSeparatedEnvList
|
|
244
|
+
};
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertReactionaryFeedClient
|
|
3
|
+
} from "./feed-types.js";
|
|
4
|
+
class ReactionaryFeedGenerator {
|
|
5
|
+
constructor(client, options = {}) {
|
|
6
|
+
this.options = options;
|
|
7
|
+
assertReactionaryFeedClient(client);
|
|
8
|
+
this.client = client;
|
|
9
|
+
}
|
|
10
|
+
options;
|
|
11
|
+
client;
|
|
12
|
+
async *products(feed, requestContext) {
|
|
13
|
+
requestContext.languageContext = feed.languageContext;
|
|
14
|
+
const pageSize = feed.pageSize ?? feed.search.paginationOptions.pageSize ?? 50;
|
|
15
|
+
const maxPages = feed.maxPages ?? 100;
|
|
16
|
+
const productConcurrency = normalizeProductConcurrency(
|
|
17
|
+
this.options.productConcurrency
|
|
18
|
+
);
|
|
19
|
+
const startedAt = Date.now();
|
|
20
|
+
let processedProducts = 0;
|
|
21
|
+
let totalProducts = void 0;
|
|
22
|
+
let pageNumber = 1;
|
|
23
|
+
this.reportProgress({
|
|
24
|
+
phase: "searching",
|
|
25
|
+
processedProducts,
|
|
26
|
+
elapsedMs: Date.now() - startedAt
|
|
27
|
+
});
|
|
28
|
+
while (pageNumber <= maxPages) {
|
|
29
|
+
const result = await unwrapFeedResult(
|
|
30
|
+
this.client.productSearch.queryByTerm({
|
|
31
|
+
search: {
|
|
32
|
+
...feed.search,
|
|
33
|
+
paginationOptions: {
|
|
34
|
+
...feed.search.paginationOptions,
|
|
35
|
+
pageNumber,
|
|
36
|
+
pageSize
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
);
|
|
41
|
+
totalProducts = estimateTotalProducts(result, pageSize, maxPages);
|
|
42
|
+
const totalPages = Math.min(result.totalPages, maxPages);
|
|
43
|
+
this.reportProgress({
|
|
44
|
+
phase: "generating",
|
|
45
|
+
processedProducts,
|
|
46
|
+
totalProducts,
|
|
47
|
+
pageNumber,
|
|
48
|
+
totalPages,
|
|
49
|
+
elapsedMs: Date.now() - startedAt
|
|
50
|
+
});
|
|
51
|
+
for (const batch of chunks(result.items, productConcurrency)) {
|
|
52
|
+
const products = await Promise.all(
|
|
53
|
+
batch.map((item) => this.toFeedProduct(item, feed))
|
|
54
|
+
);
|
|
55
|
+
for (const product of products) {
|
|
56
|
+
if (product) {
|
|
57
|
+
processedProducts += 1;
|
|
58
|
+
this.reportProgress({
|
|
59
|
+
phase: "generating",
|
|
60
|
+
processedProducts,
|
|
61
|
+
totalProducts,
|
|
62
|
+
pageNumber,
|
|
63
|
+
totalPages,
|
|
64
|
+
elapsedMs: Date.now() - startedAt
|
|
65
|
+
});
|
|
66
|
+
yield product;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (pageNumber >= result.totalPages || result.items.length === 0) {
|
|
71
|
+
this.reportProgress({
|
|
72
|
+
phase: "completed",
|
|
73
|
+
processedProducts,
|
|
74
|
+
totalProducts,
|
|
75
|
+
pageNumber,
|
|
76
|
+
totalPages,
|
|
77
|
+
elapsedMs: Date.now() - startedAt
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
pageNumber += 1;
|
|
82
|
+
}
|
|
83
|
+
this.reportProgress({
|
|
84
|
+
phase: "completed",
|
|
85
|
+
processedProducts,
|
|
86
|
+
totalProducts,
|
|
87
|
+
pageNumber: pageNumber - 1,
|
|
88
|
+
totalPages: totalProducts === void 0 ? void 0 : maxPages,
|
|
89
|
+
elapsedMs: Date.now() - startedAt
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async toFeedProduct(item, feed) {
|
|
93
|
+
const firstVariant = item.variants[0]?.variant;
|
|
94
|
+
if (!firstVariant) {
|
|
95
|
+
return void 0;
|
|
96
|
+
}
|
|
97
|
+
const productResult = await this.client.product.getBySKU({
|
|
98
|
+
variant: firstVariant
|
|
99
|
+
});
|
|
100
|
+
const product = productResult.success ? productResult.value : void 0;
|
|
101
|
+
const variants = await Promise.all(
|
|
102
|
+
item.variants.map(
|
|
103
|
+
(variant) => this.toFeedVariant(item, product, variant.variant.sku, feed)
|
|
104
|
+
)
|
|
105
|
+
);
|
|
106
|
+
const images = uniqueImages([
|
|
107
|
+
...toFeedImages(product?.mainVariant.images ?? []),
|
|
108
|
+
...item.variants.flatMap((variant) => toFeedImages([variant.image]))
|
|
109
|
+
]);
|
|
110
|
+
const [categoryPath, reviewData] = product ? await Promise.all([
|
|
111
|
+
this.getCategoryPath(product),
|
|
112
|
+
this.getReviewData(product)
|
|
113
|
+
]) : [void 0, void 0];
|
|
114
|
+
return {
|
|
115
|
+
id: product?.identifier.key ?? item.identifier.key,
|
|
116
|
+
title: product?.name ?? item.name,
|
|
117
|
+
description: product?.description || product?.longDescription,
|
|
118
|
+
url: toProductUrl(product?.slug ?? item.slug, feed),
|
|
119
|
+
brand: product?.brand,
|
|
120
|
+
manufacturer: product?.manufacturer,
|
|
121
|
+
...categoryPath && categoryPath.length > 0 ? { categoryPath } : {},
|
|
122
|
+
...reviewData?.ratingSummary ? { ratingSummary: reviewData.ratingSummary } : {},
|
|
123
|
+
...reviewData?.reviews?.length ? { reviews: reviewData.reviews } : {},
|
|
124
|
+
images,
|
|
125
|
+
variants
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
async toFeedVariant(item, product, sku, feed) {
|
|
129
|
+
const variant = product?.variants.find(
|
|
130
|
+
(productVariant) => productVariant.identifier.sku === sku
|
|
131
|
+
) ?? product?.mainVariant;
|
|
132
|
+
const [customerPriceResult, listPriceResult, inventory] = await Promise.all([
|
|
133
|
+
this.client.price.getCustomerPrice({ variant: { sku } }),
|
|
134
|
+
this.client.price.getListPrice({ variant: { sku } }),
|
|
135
|
+
this.getInventoryBySKU(sku, feed)
|
|
136
|
+
]);
|
|
137
|
+
const customerPrice = customerPriceResult.success ? {
|
|
138
|
+
value: customerPriceResult.value.unitPrice.value,
|
|
139
|
+
currency: customerPriceResult.value.unitPrice.currency
|
|
140
|
+
} : void 0;
|
|
141
|
+
const listPrice = listPriceResult.success ? {
|
|
142
|
+
value: listPriceResult.value.unitPrice.value,
|
|
143
|
+
currency: listPriceResult.value.unitPrice.currency
|
|
144
|
+
} : void 0;
|
|
145
|
+
const availability = inventory ? toFeedAvailability(inventory) : void 0;
|
|
146
|
+
return {
|
|
147
|
+
id: sku,
|
|
148
|
+
title: variant?.name || item.name,
|
|
149
|
+
description: product?.description,
|
|
150
|
+
url: toProductUrl(product?.slug ?? item.slug, feed),
|
|
151
|
+
...variant?.ean ? { ean: variant.ean } : {},
|
|
152
|
+
...variant?.gtin ? { gtin: variant.gtin } : {},
|
|
153
|
+
...variant?.upc ? { upc: variant.upc } : {},
|
|
154
|
+
...variant?.barcode ? { barcode: variant.barcode } : {},
|
|
155
|
+
manufacturerPartNumber: sku,
|
|
156
|
+
images: uniqueImages(toFeedImages(variant?.images ?? [])),
|
|
157
|
+
price: customerPrice,
|
|
158
|
+
listPrice,
|
|
159
|
+
availability,
|
|
160
|
+
barcodes: toFeedBarcodes(variant),
|
|
161
|
+
options: variant?.options.map((option) => ({
|
|
162
|
+
name: option.name,
|
|
163
|
+
value: option.value.label || option.value.identifier.key
|
|
164
|
+
})) ?? []
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
async getInventoryBySKU(sku, feed) {
|
|
168
|
+
const fulfillmentCenterKeys = getFulfillmentCenterKeys(
|
|
169
|
+
feed,
|
|
170
|
+
this.options.defaultFulfillmentCenterKeys
|
|
171
|
+
);
|
|
172
|
+
const inventoryResults = await Promise.all(
|
|
173
|
+
fulfillmentCenterKeys.map(
|
|
174
|
+
(fulfillmentCenterKey) => this.client.inventory.getBySKU({
|
|
175
|
+
variant: { sku },
|
|
176
|
+
fulfilmentCenter: {
|
|
177
|
+
key: fulfillmentCenterKey
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
)
|
|
181
|
+
);
|
|
182
|
+
const inventories = inventoryResults.filter((result) => result.success).map((result) => result.value);
|
|
183
|
+
if (inventories.length === 0) {
|
|
184
|
+
return void 0;
|
|
185
|
+
}
|
|
186
|
+
if (inventories.length === 1) {
|
|
187
|
+
return inventories[0];
|
|
188
|
+
}
|
|
189
|
+
return combineInventory(sku, inventories);
|
|
190
|
+
}
|
|
191
|
+
async getCategoryPath(product) {
|
|
192
|
+
const category = this.client.category;
|
|
193
|
+
const getBreadcrumbPathToCategory = category?.getBreadcrumbPathToCategory;
|
|
194
|
+
const parentCategory = product.parentCategories[0];
|
|
195
|
+
if (!getBreadcrumbPathToCategory || !parentCategory) {
|
|
196
|
+
return void 0;
|
|
197
|
+
}
|
|
198
|
+
const result = await getBreadcrumbPathToCategory.call(category, {
|
|
199
|
+
id: parentCategory
|
|
200
|
+
});
|
|
201
|
+
if (!result.success) {
|
|
202
|
+
throw new Error(JSON.stringify(result.error));
|
|
203
|
+
}
|
|
204
|
+
return result.value.map(toFeedCategory);
|
|
205
|
+
}
|
|
206
|
+
async getReviewData(product) {
|
|
207
|
+
const productReviews = this.client.productReviews;
|
|
208
|
+
if (!productReviews) {
|
|
209
|
+
return void 0;
|
|
210
|
+
}
|
|
211
|
+
const [summaryResult, reviewsResult] = await Promise.all([
|
|
212
|
+
productReviews.getRatingSummary({
|
|
213
|
+
product: product.identifier
|
|
214
|
+
}),
|
|
215
|
+
productReviews.findReviews({
|
|
216
|
+
product: product.identifier,
|
|
217
|
+
paginationOptions: {
|
|
218
|
+
pageNumber: 1,
|
|
219
|
+
pageSize: 3
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
]);
|
|
223
|
+
if (!summaryResult.success) {
|
|
224
|
+
throw new Error(JSON.stringify(summaryResult.error));
|
|
225
|
+
}
|
|
226
|
+
if (!reviewsResult.success) {
|
|
227
|
+
throw new Error(JSON.stringify(reviewsResult.error));
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
ratingSummary: toFeedRatingSummary(summaryResult.value),
|
|
231
|
+
reviews: reviewsResult.value.items.map(toFeedReview)
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
reportProgress(progress) {
|
|
235
|
+
this.options.onProgress?.(progress);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async function unwrapFeedResult(resultPromise) {
|
|
239
|
+
const result = await resultPromise;
|
|
240
|
+
if (!result.success) {
|
|
241
|
+
throw new Error(JSON.stringify(result.error));
|
|
242
|
+
}
|
|
243
|
+
return result.value;
|
|
244
|
+
}
|
|
245
|
+
function toFeedAvailability(inventory) {
|
|
246
|
+
return {
|
|
247
|
+
available: inventory.status === "inStock" && inventory.quantity > 0,
|
|
248
|
+
status: toFeedAvailabilityStatus(inventory.status),
|
|
249
|
+
quantity: inventory.quantity
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function toFeedCategory(category) {
|
|
253
|
+
return {
|
|
254
|
+
id: category.identifier.key,
|
|
255
|
+
name: category.name,
|
|
256
|
+
...category.slug ? { slug: category.slug } : {}
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function toFeedRatingSummary(summary) {
|
|
260
|
+
return {
|
|
261
|
+
averageRating: summary.averageRating,
|
|
262
|
+
...summary.totalRatings !== void 0 ? { totalRatings: summary.totalRatings } : {},
|
|
263
|
+
...summary.ratingDistribution ? { ratingDistribution: summary.ratingDistribution } : {}
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function toFeedReview(review) {
|
|
267
|
+
return {
|
|
268
|
+
id: review.identifier.key,
|
|
269
|
+
authorName: review.authorName,
|
|
270
|
+
rating: review.rating,
|
|
271
|
+
title: review.title,
|
|
272
|
+
content: review.content,
|
|
273
|
+
createdAt: review.createdAt,
|
|
274
|
+
verified: review.verified
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function estimateTotalProducts(result, pageSize, maxPages) {
|
|
278
|
+
return Math.min(result.totalCount, Math.min(result.totalPages, maxPages) * pageSize);
|
|
279
|
+
}
|
|
280
|
+
function normalizeProductConcurrency(value) {
|
|
281
|
+
if (!value || !Number.isFinite(value)) {
|
|
282
|
+
return 10;
|
|
283
|
+
}
|
|
284
|
+
return Math.max(1, Math.floor(value));
|
|
285
|
+
}
|
|
286
|
+
function chunks(items, size) {
|
|
287
|
+
const result = [];
|
|
288
|
+
for (let index = 0; index < items.length; index += size) {
|
|
289
|
+
result.push(items.slice(index, index + size));
|
|
290
|
+
}
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
293
|
+
function toFeedAvailabilityStatus(status) {
|
|
294
|
+
switch (status) {
|
|
295
|
+
case "inStock":
|
|
296
|
+
return "in_stock";
|
|
297
|
+
case "onBackOrder":
|
|
298
|
+
return "backorder";
|
|
299
|
+
case "preOrder":
|
|
300
|
+
return "preorder";
|
|
301
|
+
case "discontinued":
|
|
302
|
+
return "discontinued";
|
|
303
|
+
case "outOfStock":
|
|
304
|
+
default:
|
|
305
|
+
return "out_of_stock";
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function getFulfillmentCenterKeys(feed, defaultFulfillmentCenterKeys) {
|
|
309
|
+
const keys = feed.fulfillmentCenterKeys?.length ? feed.fulfillmentCenterKeys : feed.fulfillmentCenterKey ? [feed.fulfillmentCenterKey] : defaultFulfillmentCenterKeys?.length ? defaultFulfillmentCenterKeys : [""];
|
|
310
|
+
return [...new Set(keys.map((key) => key.trim()))];
|
|
311
|
+
}
|
|
312
|
+
function combineInventory(sku, inventories) {
|
|
313
|
+
return {
|
|
314
|
+
identifier: {
|
|
315
|
+
variant: { sku },
|
|
316
|
+
fulfillmentCenter: {
|
|
317
|
+
key: inventories.map(
|
|
318
|
+
(inventory) => inventory.identifier.fulfillmentCenter.key
|
|
319
|
+
).join(",")
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
quantity: inventories.reduce((total, inventory) => total + inventory.quantity, 0),
|
|
323
|
+
status: getCombinedInventoryStatus(inventories)
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function getCombinedInventoryStatus(inventories) {
|
|
327
|
+
if (inventories.some(
|
|
328
|
+
(inventory) => inventory.status === "inStock" && inventory.quantity > 0
|
|
329
|
+
)) {
|
|
330
|
+
return "inStock";
|
|
331
|
+
}
|
|
332
|
+
if (inventories.some((inventory) => inventory.status === "onBackOrder")) {
|
|
333
|
+
return "onBackOrder";
|
|
334
|
+
}
|
|
335
|
+
if (inventories.some((inventory) => inventory.status === "preOrder")) {
|
|
336
|
+
return "preOrder";
|
|
337
|
+
}
|
|
338
|
+
if (inventories.every((inventory) => inventory.status === "discontinued")) {
|
|
339
|
+
return "discontinued";
|
|
340
|
+
}
|
|
341
|
+
return "outOfStock";
|
|
342
|
+
}
|
|
343
|
+
function toFeedImages(images) {
|
|
344
|
+
return images.filter((image) => image.sourceUrl.length > 0).map((image) => ({
|
|
345
|
+
url: image.sourceUrl,
|
|
346
|
+
...image.altText ? { altText: image.altText } : {}
|
|
347
|
+
}));
|
|
348
|
+
}
|
|
349
|
+
function uniqueImages(images) {
|
|
350
|
+
const seen = /* @__PURE__ */ new Set();
|
|
351
|
+
return images.filter((image) => {
|
|
352
|
+
if (seen.has(image.url)) {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
seen.add(image.url);
|
|
356
|
+
return true;
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
function toFeedBarcodes(variant) {
|
|
360
|
+
if (!variant) {
|
|
361
|
+
return [];
|
|
362
|
+
}
|
|
363
|
+
return [
|
|
364
|
+
["ean", variant.ean],
|
|
365
|
+
["gtin", variant.gtin],
|
|
366
|
+
["upc", variant.upc],
|
|
367
|
+
["barcode", variant.barcode]
|
|
368
|
+
].filter(([, value]) => value.length > 0).map(([type, value]) => ({
|
|
369
|
+
type,
|
|
370
|
+
value
|
|
371
|
+
}));
|
|
372
|
+
}
|
|
373
|
+
function toProductUrl(slug, feed) {
|
|
374
|
+
if (!slug || !feed.productUrlBase) {
|
|
375
|
+
return void 0;
|
|
376
|
+
}
|
|
377
|
+
if (feed.productUrlBase.includes("{slug}") || feed.productUrlBase.includes("{lang}")) {
|
|
378
|
+
return feed.productUrlBase.replaceAll("{lang}", getLanguage(feed.languageContext)).replaceAll("{slug}", encodeURIComponent(slug));
|
|
379
|
+
}
|
|
380
|
+
return new URL(slug, feed.productUrlBase).href;
|
|
381
|
+
}
|
|
382
|
+
function getLanguage(feed) {
|
|
383
|
+
const [language] = feed.locale.split("-");
|
|
384
|
+
return language.toLowerCase();
|
|
385
|
+
}
|
|
386
|
+
function getTargetCountry(feed) {
|
|
387
|
+
const [, region] = feed.locale.split("-");
|
|
388
|
+
return region?.toUpperCase();
|
|
389
|
+
}
|
|
390
|
+
export {
|
|
391
|
+
ReactionaryFeedGenerator,
|
|
392
|
+
getLanguage,
|
|
393
|
+
getTargetCountry,
|
|
394
|
+
toProductUrl
|
|
395
|
+
};
|