@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,128 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getTargetCountry
|
|
3
|
+
} from "../feed-generator.js";
|
|
4
|
+
import {
|
|
5
|
+
formatMoney,
|
|
6
|
+
primaryImage,
|
|
7
|
+
productDescription
|
|
8
|
+
} from "./shared.js";
|
|
9
|
+
const acpProductFeedTransformer = {
|
|
10
|
+
id: "acp-product-feed",
|
|
11
|
+
title: "ACP Product Feed",
|
|
12
|
+
description: "Agentic Commerce Protocol product feed output.",
|
|
13
|
+
output: {
|
|
14
|
+
contentType: "application/x-ndjson; charset=utf-8",
|
|
15
|
+
fileExtension: "jsonl"
|
|
16
|
+
},
|
|
17
|
+
defaultOptions: {
|
|
18
|
+
format: "jsonl"
|
|
19
|
+
},
|
|
20
|
+
async *transform(products, context) {
|
|
21
|
+
if (context.options.format === "json") {
|
|
22
|
+
const collected = [];
|
|
23
|
+
for await (const product of products) {
|
|
24
|
+
collected.push(...toACPRows(product, context.feed));
|
|
25
|
+
}
|
|
26
|
+
yield JSON.stringify({
|
|
27
|
+
...getTargetCountry(context.feed.languageContext) ? { target_country: getTargetCountry(context.feed.languageContext) } : {},
|
|
28
|
+
products: collected
|
|
29
|
+
});
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
for await (const product of products) {
|
|
33
|
+
for (const row of toACPRows(product, context.feed)) {
|
|
34
|
+
yield `${JSON.stringify(row)}
|
|
35
|
+
`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
function toACPRows(product, feed) {
|
|
41
|
+
return product.variants.map(
|
|
42
|
+
(variant) => toACPRow(product, variant, feed)
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
function toACPRow(product, variant, feed) {
|
|
46
|
+
const primaryImageUrl = primaryImage(product, variant);
|
|
47
|
+
const additionalImageUrls = getAdditionalImageUrls(product, variant, primaryImageUrl);
|
|
48
|
+
const variation = getVariationFields(product, variant);
|
|
49
|
+
const priceFields = getPriceFields(variant);
|
|
50
|
+
const gtin = variant.gtin || variant.ean || variant.upc || void 0;
|
|
51
|
+
return {
|
|
52
|
+
item_id: variant.id,
|
|
53
|
+
...variation,
|
|
54
|
+
title: variant.title || product.title,
|
|
55
|
+
description: variant.description ?? productDescription(product),
|
|
56
|
+
...variant.url ?? product.url ? { url: variant.url ?? product.url } : {},
|
|
57
|
+
...product.brand ? { brand: product.brand } : {},
|
|
58
|
+
is_eligible_search: true,
|
|
59
|
+
...feed.sellerName ? { seller_name: feed.sellerName } : {},
|
|
60
|
+
...product.manufacturer ? { manufacturer: product.manufacturer } : {},
|
|
61
|
+
...product.categoryPath?.length ? {
|
|
62
|
+
product_category: product.categoryPath.map((category) => category.name).join(" > ")
|
|
63
|
+
} : {},
|
|
64
|
+
...product.ratingSummary ? {
|
|
65
|
+
...product.ratingSummary.totalRatings !== void 0 ? { review_count: product.ratingSummary.totalRatings } : {},
|
|
66
|
+
...product.ratingSummary.totalRatings ? { star_rating: product.ratingSummary.averageRating.toFixed(2) } : {}
|
|
67
|
+
} : {},
|
|
68
|
+
...primaryImageUrl ? { image_url: primaryImageUrl } : {},
|
|
69
|
+
...additionalImageUrls.length > 0 ? { additional_image_urls: additionalImageUrls } : {},
|
|
70
|
+
availability: toACPAvailability(variant.availability?.status),
|
|
71
|
+
...priceFields,
|
|
72
|
+
...gtin ? { gtin } : {},
|
|
73
|
+
...variant.manufacturerPartNumber ? { mpn: variant.manufacturerPartNumber } : {}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function getVariationFields(product, variant) {
|
|
77
|
+
if (product.variants.length <= 1 || product.id === variant.id || variant.options.length === 0) {
|
|
78
|
+
return {};
|
|
79
|
+
}
|
|
80
|
+
const variantDict = Object.fromEntries(
|
|
81
|
+
variant.options.map((option) => [option.name, option.value])
|
|
82
|
+
);
|
|
83
|
+
return {
|
|
84
|
+
group_id: product.id,
|
|
85
|
+
listing_has_variations: true,
|
|
86
|
+
variant_dict: variantDict
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function getPriceFields(variant) {
|
|
90
|
+
if (variant.price && variant.listPrice && variant.price.currency.toUpperCase() === variant.listPrice.currency.toUpperCase() && variant.price.value > 0 && variant.listPrice.value > variant.price.value) {
|
|
91
|
+
return {
|
|
92
|
+
price: formatMoney(variant.listPrice),
|
|
93
|
+
sale_price: formatMoney(variant.price)
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
...variant.price ?? variant.listPrice ? { price: formatMoney(variant.price ?? variant.listPrice) } : {}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function toACPAvailability(status) {
|
|
101
|
+
switch (status) {
|
|
102
|
+
case "in_stock":
|
|
103
|
+
case "out_of_stock":
|
|
104
|
+
case "backorder":
|
|
105
|
+
return status;
|
|
106
|
+
case "preorder":
|
|
107
|
+
return "pre_order";
|
|
108
|
+
default:
|
|
109
|
+
return "unknown";
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function getAdditionalImageUrls(product, variant, primaryImageUrl) {
|
|
113
|
+
const urls = [
|
|
114
|
+
...variant.images.map((image) => image.url),
|
|
115
|
+
...product.images.map((image) => image.url)
|
|
116
|
+
];
|
|
117
|
+
const unique = /* @__PURE__ */ new Set();
|
|
118
|
+
return urls.filter((url) => {
|
|
119
|
+
if (url === primaryImageUrl || unique.has(url)) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
unique.add(url);
|
|
123
|
+
return true;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
export {
|
|
127
|
+
acpProductFeedTransformer
|
|
128
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { create } from "xmlbuilder2";
|
|
2
|
+
import {
|
|
3
|
+
googleMoney,
|
|
4
|
+
primaryImage,
|
|
5
|
+
productDescription
|
|
6
|
+
} from "./shared.js";
|
|
7
|
+
const googleMerchantFeedTransformer = {
|
|
8
|
+
id: "google-merchant-feed",
|
|
9
|
+
title: "Google Merchant Feed",
|
|
10
|
+
description: "Google Merchant Center RSS XML feed.",
|
|
11
|
+
output: {
|
|
12
|
+
contentType: "application/xml; charset=utf-8",
|
|
13
|
+
fileExtension: "xml"
|
|
14
|
+
},
|
|
15
|
+
async *transform(products) {
|
|
16
|
+
const document = create({ version: "1.0", encoding: "UTF-8" });
|
|
17
|
+
const channel = document.ele("rss", {
|
|
18
|
+
version: "2.0",
|
|
19
|
+
"xmlns:g": "http://base.google.com/ns/1.0"
|
|
20
|
+
}).ele("channel");
|
|
21
|
+
channel.ele("title").txt("Reactionary Product Feed").up();
|
|
22
|
+
for await (const product of products) {
|
|
23
|
+
for (const variant of product.variants) {
|
|
24
|
+
appendGoogleItem(channel, product, variant);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
yield `${document.end({ prettyPrint: true })}
|
|
28
|
+
`;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
function appendGoogleItem(channel, product, variant) {
|
|
32
|
+
const item = channel.ele("item");
|
|
33
|
+
const image = primaryImage(product, variant);
|
|
34
|
+
const link = variant.url ?? product.url;
|
|
35
|
+
const gtin = variant.barcodes.find((barcode) => barcode.type === "gtin")?.value;
|
|
36
|
+
item.ele("g:id").txt(variant.id).up();
|
|
37
|
+
item.ele("g:item_group_id").txt(product.id).up();
|
|
38
|
+
item.ele("title").txt(variant.title || product.title).up();
|
|
39
|
+
item.ele("description").txt(variant.description ?? productDescription(product)).up();
|
|
40
|
+
if (link) {
|
|
41
|
+
item.ele("link").txt(link).up();
|
|
42
|
+
}
|
|
43
|
+
if (image) {
|
|
44
|
+
item.ele("g:image_link").txt(image).up();
|
|
45
|
+
}
|
|
46
|
+
item.ele("g:availability").txt(variant.availability?.available ? "in stock" : "out of stock").up();
|
|
47
|
+
if (variant.price) {
|
|
48
|
+
item.ele("g:price").txt(googleMoney(variant.price) ?? "").up();
|
|
49
|
+
}
|
|
50
|
+
if (product.brand) {
|
|
51
|
+
item.ele("g:brand").txt(product.brand).up();
|
|
52
|
+
}
|
|
53
|
+
if (gtin) {
|
|
54
|
+
item.ele("g:gtin").txt(gtin).up();
|
|
55
|
+
}
|
|
56
|
+
item.ele("g:condition").txt("new").up();
|
|
57
|
+
}
|
|
58
|
+
export {
|
|
59
|
+
googleMerchantFeedTransformer
|
|
60
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { create } from "xmlbuilder2";
|
|
2
|
+
import {
|
|
3
|
+
formatMoney,
|
|
4
|
+
primaryImage,
|
|
5
|
+
productDescription
|
|
6
|
+
} from "./shared.js";
|
|
7
|
+
const pricerunnerFeedTransformer = {
|
|
8
|
+
id: "pricerunner-feed",
|
|
9
|
+
title: "PriceRunner XML Feed",
|
|
10
|
+
description: "PriceRunner Products XML feed.",
|
|
11
|
+
output: {
|
|
12
|
+
contentType: "application/xml; charset=utf-8",
|
|
13
|
+
fileExtension: "xml"
|
|
14
|
+
},
|
|
15
|
+
async *transform(products) {
|
|
16
|
+
const document = create({ version: "1.0", encoding: "UTF-8" });
|
|
17
|
+
const root = document.ele("Products");
|
|
18
|
+
for await (const product of products) {
|
|
19
|
+
for (const variant of product.variants) {
|
|
20
|
+
appendPriceRunnerProduct(root, product, variant);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
yield `${document.end({ prettyPrint: true })}
|
|
24
|
+
`;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
function appendPriceRunnerProduct(root, product, variant) {
|
|
28
|
+
const ean = variant.barcodes.find((barcode) => barcode.type === "ean")?.value ?? variant.barcodes.find((barcode) => barcode.type === "gtin")?.value;
|
|
29
|
+
const url = variant.url ?? product.url;
|
|
30
|
+
const image = primaryImage(product, variant);
|
|
31
|
+
const productElement = root.ele("Product");
|
|
32
|
+
productElement.ele("ProductId").txt(variant.id).up();
|
|
33
|
+
productElement.ele("ProductName").dat(variant.title || product.title).up();
|
|
34
|
+
if (variant.price) {
|
|
35
|
+
productElement.ele("Price").txt(formatMoney(variant.price) ?? "").up();
|
|
36
|
+
}
|
|
37
|
+
productElement.ele("StockStatus").txt(variant.availability?.available ? "in stock" : "out of stock").up();
|
|
38
|
+
if (product.brand) {
|
|
39
|
+
productElement.ele("Brand").dat(product.brand).up();
|
|
40
|
+
}
|
|
41
|
+
productElement.ele("Msku").txt(variant.id).up();
|
|
42
|
+
if (ean) {
|
|
43
|
+
productElement.ele("Ean").txt(ean).up();
|
|
44
|
+
}
|
|
45
|
+
if (url) {
|
|
46
|
+
productElement.ele("Url").txt(url).up();
|
|
47
|
+
}
|
|
48
|
+
if (image) {
|
|
49
|
+
productElement.ele("ImageUrl").txt(image).up();
|
|
50
|
+
}
|
|
51
|
+
productElement.ele("Description").dat(variant.description ?? productDescription(product)).up();
|
|
52
|
+
productElement.ele("Condition").txt("New").up();
|
|
53
|
+
productElement.ele("GroupId").txt(product.id).up();
|
|
54
|
+
for (const option of variant.options) {
|
|
55
|
+
productElement.ele(option.name).dat(option.value).up();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export {
|
|
59
|
+
pricerunnerFeedTransformer
|
|
60
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
function toMinorUnits(value) {
|
|
2
|
+
return Math.max(Math.round(value * 100), 0);
|
|
3
|
+
}
|
|
4
|
+
function formatMoney(money) {
|
|
5
|
+
if (!money) {
|
|
6
|
+
return void 0;
|
|
7
|
+
}
|
|
8
|
+
return `${money.value.toFixed(2)} ${money.currency.toUpperCase()}`;
|
|
9
|
+
}
|
|
10
|
+
function googleMoney(money) {
|
|
11
|
+
return formatMoney(money);
|
|
12
|
+
}
|
|
13
|
+
function primaryImage(product, variant) {
|
|
14
|
+
return variant?.images[0]?.url ?? product.images[0]?.url;
|
|
15
|
+
}
|
|
16
|
+
function productDescription(product) {
|
|
17
|
+
return product.description ?? product.title;
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
formatMoney,
|
|
21
|
+
googleMoney,
|
|
22
|
+
primaryImage,
|
|
23
|
+
productDescription,
|
|
24
|
+
toMinorUnits
|
|
25
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { toSitemapXml } from "../sitemap-xml.js";
|
|
2
|
+
const sitemapFeedTransformer = {
|
|
3
|
+
id: "sitemap-feed",
|
|
4
|
+
title: "Sitemap XML Feed",
|
|
5
|
+
description: "XML sitemap of product URLs.",
|
|
6
|
+
output: {
|
|
7
|
+
contentType: "application/xml; charset=utf-8",
|
|
8
|
+
fileExtension: "xml"
|
|
9
|
+
},
|
|
10
|
+
async *transform(products) {
|
|
11
|
+
yield toSitemapXml(toProductSitemapEntries(products));
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
async function* toProductSitemapEntries(products) {
|
|
15
|
+
for await (const product of products) {
|
|
16
|
+
if (!product.url) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
yield {
|
|
20
|
+
url: product.url
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export {
|
|
25
|
+
sitemapFeedTransformer
|
|
26
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@reactionary/feeds",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./index.js",
|
|
6
|
+
"types": "./src/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"reactionary-feeds": "./bin/reactionary-feeds"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/Solteq/reactionary.git"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@reactionary/algolia": "0.0.1",
|
|
16
|
+
"@reactionary/commercetools": "0.0.1",
|
|
17
|
+
"@reactionary/core": "0.0.1",
|
|
18
|
+
"@reactionary/fake": "0.0.1",
|
|
19
|
+
"@reactionary/magento": "0.0.1",
|
|
20
|
+
"@reactionary/medusa": "0.0.1",
|
|
21
|
+
"@reactionary/meilisearch": "0.0.1",
|
|
22
|
+
"@reactionary/unomi": "0.0.1",
|
|
23
|
+
"commander": "^15.0.0",
|
|
24
|
+
"dotenv": "^17.2.2",
|
|
25
|
+
"sitemap": "^9.0.1",
|
|
26
|
+
"xmlbuilder2": "^4.0.3"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"vitest": "^4.0.9"
|
|
30
|
+
},
|
|
31
|
+
"sideEffects": false
|
|
32
|
+
}
|
package/src/cli.d.ts
ADDED
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from './lib/default-registry.js';
|
|
2
|
+
export * from './lib/env-client-builder.js';
|
|
3
|
+
export * from './lib/feed-generator.js';
|
|
4
|
+
export * from './lib/feed-server.js';
|
|
5
|
+
export * from './lib/feed-transformer.js';
|
|
6
|
+
export * from './lib/feed-types.js';
|
|
7
|
+
export * from './lib/sitemap-generator.js';
|
|
8
|
+
export * from './lib/sitemap-xml.js';
|
|
9
|
+
export * from './lib/load-cwd-env.js';
|
|
10
|
+
export * from './lib/transformers/acp-product-feed.transformer.js';
|
|
11
|
+
export * from './lib/transformers/google-merchant-feed.transformer.js';
|
|
12
|
+
export * from './lib/transformers/pricerunner-feed.transformer.js';
|
|
13
|
+
export * from './lib/transformers/sitemap-feed.transformer.js';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Cache, type RequestContext } from '@reactionary/core';
|
|
2
|
+
import type { ReactionaryFeedClient, ReactionaryFeedClientFactory } from './feed-types.js';
|
|
3
|
+
export type ReactionaryFeedProviderSystem = 'ALGOLIA' | 'COMMERCETOOLS' | 'FAKE' | 'MAGENTO' | 'MEDUSA' | 'MEILISEARCH' | 'UNOMI';
|
|
4
|
+
export interface CreateReactionaryFeedClientFromEnvOptions {
|
|
5
|
+
env?: NodeJS.ProcessEnv;
|
|
6
|
+
contextOverrides?: Partial<RequestContext>;
|
|
7
|
+
cache?: Cache;
|
|
8
|
+
loadEnv?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface ReactionaryFeedClientFromEnv {
|
|
11
|
+
client: ReactionaryFeedClient;
|
|
12
|
+
enabledSystems: ReactionaryFeedProviderSystem[];
|
|
13
|
+
}
|
|
14
|
+
export declare function createReactionaryFeedClientFromEnv(options?: CreateReactionaryFeedClientFromEnvOptions): ReactionaryFeedClientFromEnv;
|
|
15
|
+
export declare function createReactionaryFeedClientFactoryFromEnv(options?: Omit<CreateReactionaryFeedClientFromEnvOptions, 'contextOverrides'>): ReactionaryFeedClientFactory;
|
|
16
|
+
export declare function getEnabledReactionaryFeedProviderSystems(env?: NodeJS.ProcessEnv): ReactionaryFeedProviderSystem[];
|
|
17
|
+
export declare function getNoEnabledProviderSystemsMessage(): string;
|
|
18
|
+
export declare function parseCommaSeparatedEnvList(value: string | undefined): string[];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { RequestContext } from '@reactionary/core';
|
|
2
|
+
import { type ReactionaryFeedClient, type ReactionaryFeedDefinition, type ReactionaryFeedGeneratorOptions, type ReactionaryFeedProduct } from './feed-types.js';
|
|
3
|
+
export declare class ReactionaryFeedGenerator {
|
|
4
|
+
private readonly options;
|
|
5
|
+
private readonly client;
|
|
6
|
+
constructor(client: ReactionaryFeedClient, options?: ReactionaryFeedGeneratorOptions);
|
|
7
|
+
products(feed: ReactionaryFeedDefinition, requestContext: RequestContext): AsyncGenerator<ReactionaryFeedProduct>;
|
|
8
|
+
private toFeedProduct;
|
|
9
|
+
private toFeedVariant;
|
|
10
|
+
private getInventoryBySKU;
|
|
11
|
+
private getCategoryPath;
|
|
12
|
+
private getReviewData;
|
|
13
|
+
private reportProgress;
|
|
14
|
+
}
|
|
15
|
+
export declare function toProductUrl(slug: string | undefined, feed: ReactionaryFeedDefinition): string | undefined;
|
|
16
|
+
export declare function getLanguage(feed: ReactionaryFeedDefinition['languageContext']): string;
|
|
17
|
+
export declare function getTargetCountry(feed: ReactionaryFeedDefinition['languageContext']): string | undefined;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import type { ReactionaryFeedTransformerRegistry } from './feed-transformer.js';
|
|
3
|
+
import type { ReactionaryFeedClient, ReactionaryFeedClientFactory, ReactionaryFeedDefinition, ReactionaryFeedInventoryOptions, ReactionaryFeedProcessingOptions, ReactionarySitemapOptions } from './feed-types.js';
|
|
4
|
+
export interface ReactionaryFeedServerOptions extends ReactionaryFeedInventoryOptions, ReactionaryFeedProcessingOptions {
|
|
5
|
+
basePath?: string;
|
|
6
|
+
feeds: Record<string, ReactionaryFeedDefinition>;
|
|
7
|
+
sitemaps?: ReactionarySitemapOptions;
|
|
8
|
+
registry?: ReactionaryFeedTransformerRegistry;
|
|
9
|
+
}
|
|
10
|
+
export interface ReactionaryFeedHttpHandler {
|
|
11
|
+
fetch(request: Request): Promise<Response>;
|
|
12
|
+
close(): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export type ReactionaryFeedNodeRequestHandler = (request: IncomingMessage, response: ServerResponse) => Promise<void>;
|
|
15
|
+
export declare class ReactionaryFeedServer<TClient extends ReactionaryFeedClient = ReactionaryFeedClient> {
|
|
16
|
+
private readonly clientFactory;
|
|
17
|
+
private readonly options;
|
|
18
|
+
private readonly registry;
|
|
19
|
+
constructor(clientFactory: ReactionaryFeedClientFactory<TClient>, options: ReactionaryFeedServerOptions);
|
|
20
|
+
fetch(request: Request): Promise<Response>;
|
|
21
|
+
getHandler(): ReactionaryFeedHttpHandler;
|
|
22
|
+
toNodeHandler(): ReactionaryFeedNodeRequestHandler;
|
|
23
|
+
close(): Promise<void>;
|
|
24
|
+
private handleRequest;
|
|
25
|
+
private generateFeedResponse;
|
|
26
|
+
private generateSitemapIndexResponse;
|
|
27
|
+
private generateSitemapResponse;
|
|
28
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ReactionaryFeedDefinition, ReactionaryFeedProduct } from './feed-types.js';
|
|
2
|
+
export interface ReactionaryFeedTransformContext<TOptions = unknown> {
|
|
3
|
+
feedId: string;
|
|
4
|
+
feed: ReactionaryFeedDefinition;
|
|
5
|
+
options: TOptions;
|
|
6
|
+
}
|
|
7
|
+
export interface ReactionaryFeedTransformer<TOptions = unknown> {
|
|
8
|
+
id: string;
|
|
9
|
+
title: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
output: {
|
|
12
|
+
contentType: string;
|
|
13
|
+
fileExtension: string;
|
|
14
|
+
};
|
|
15
|
+
defaultOptions?: TOptions;
|
|
16
|
+
transform(products: AsyncIterable<ReactionaryFeedProduct>, context: ReactionaryFeedTransformContext<TOptions>): AsyncIterable<string | Uint8Array>;
|
|
17
|
+
}
|
|
18
|
+
export interface ReactionaryFeedTransformerSummary {
|
|
19
|
+
id: string;
|
|
20
|
+
title: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
contentType: string;
|
|
23
|
+
fileExtension: string;
|
|
24
|
+
}
|
|
25
|
+
export declare class ReactionaryFeedTransformerRegistry {
|
|
26
|
+
private readonly transformers;
|
|
27
|
+
register(transformer: ReactionaryFeedTransformer): void;
|
|
28
|
+
get(id: string): ReactionaryFeedTransformer | undefined;
|
|
29
|
+
list(): ReactionaryFeedTransformerSummary[];
|
|
30
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import type { Category, CategoryPaginatedResult, Cache, Inventory, LanguageContext, Price, Product, ProductRatingSummary, ProductReviewPaginatedResult, ProductSearchResult, RequestContext, Result, SearchIdentifier, Store } from '@reactionary/core';
|
|
2
|
+
export interface ReactionaryFeedClient {
|
|
3
|
+
cache?: Cache;
|
|
4
|
+
productSearch?: {
|
|
5
|
+
queryByTerm(payload: unknown): Promise<Result<ProductSearchResult>>;
|
|
6
|
+
};
|
|
7
|
+
product?: {
|
|
8
|
+
getBySKU(payload: unknown): Promise<Result<Product>>;
|
|
9
|
+
};
|
|
10
|
+
productReviews?: {
|
|
11
|
+
getRatingSummary(payload: unknown): Promise<Result<ProductRatingSummary>>;
|
|
12
|
+
findReviews(payload: unknown): Promise<Result<ProductReviewPaginatedResult>>;
|
|
13
|
+
};
|
|
14
|
+
price?: {
|
|
15
|
+
getCustomerPrice(payload: unknown): Promise<Result<Price>>;
|
|
16
|
+
getListPrice(payload: unknown): Promise<Result<Price>>;
|
|
17
|
+
};
|
|
18
|
+
inventory?: {
|
|
19
|
+
getBySKU(payload: unknown): Promise<Result<Inventory>>;
|
|
20
|
+
};
|
|
21
|
+
category?: {
|
|
22
|
+
getBreadcrumbPathToCategory?(payload: unknown): Promise<Result<Category[]>>;
|
|
23
|
+
findTopCategories(payload: unknown): Promise<Result<CategoryPaginatedResult>>;
|
|
24
|
+
findChildCategories(payload: unknown): Promise<Result<CategoryPaginatedResult>>;
|
|
25
|
+
};
|
|
26
|
+
store?: {
|
|
27
|
+
queryByProximity(payload: unknown): Promise<Result<Store[]>>;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export type ValidatedReactionaryFeedClient = ReactionaryFeedClient & {
|
|
31
|
+
productSearch: NonNullable<ReactionaryFeedClient['productSearch']>;
|
|
32
|
+
product: NonNullable<ReactionaryFeedClient['product']>;
|
|
33
|
+
price: NonNullable<ReactionaryFeedClient['price']>;
|
|
34
|
+
inventory: NonNullable<ReactionaryFeedClient['inventory']>;
|
|
35
|
+
};
|
|
36
|
+
export type ReactionaryFeedClientFactory<TClient extends ReactionaryFeedClient = ReactionaryFeedClient> = (requestContext: RequestContext) => TClient;
|
|
37
|
+
export interface ReactionaryFeedInventoryOptions {
|
|
38
|
+
defaultFulfillmentCenterKeys?: string[];
|
|
39
|
+
}
|
|
40
|
+
export interface ReactionaryFeedProcessingOptions {
|
|
41
|
+
productConcurrency?: number;
|
|
42
|
+
}
|
|
43
|
+
export interface ReactionaryFeedProgress {
|
|
44
|
+
phase: 'searching' | 'generating' | 'completed';
|
|
45
|
+
processedProducts: number;
|
|
46
|
+
totalProducts?: number;
|
|
47
|
+
pageNumber?: number;
|
|
48
|
+
totalPages?: number;
|
|
49
|
+
elapsedMs: number;
|
|
50
|
+
}
|
|
51
|
+
export interface ReactionaryFeedGeneratorOptions extends ReactionaryFeedInventoryOptions, ReactionaryFeedProcessingOptions {
|
|
52
|
+
onProgress?: (progress: ReactionaryFeedProgress) => void;
|
|
53
|
+
}
|
|
54
|
+
export interface ReactionaryFeedDefinition {
|
|
55
|
+
languageContext: LanguageContext;
|
|
56
|
+
search: SearchIdentifier;
|
|
57
|
+
pageSize?: number;
|
|
58
|
+
maxPages?: number;
|
|
59
|
+
fulfillmentCenterKeys?: string[];
|
|
60
|
+
fulfillmentCenterKey?: string;
|
|
61
|
+
productUrlBase?: string;
|
|
62
|
+
sellerName?: string;
|
|
63
|
+
metadata?: Record<string, unknown>;
|
|
64
|
+
}
|
|
65
|
+
export type ReactionarySitemapChangeFrequency = 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';
|
|
66
|
+
export interface ReactionarySitemapEntry {
|
|
67
|
+
url: string;
|
|
68
|
+
lastmod?: Date | string;
|
|
69
|
+
changefreq?: ReactionarySitemapChangeFrequency;
|
|
70
|
+
priority?: number;
|
|
71
|
+
}
|
|
72
|
+
export interface ReactionarySitemapBaseDefinition {
|
|
73
|
+
languageContext: LanguageContext;
|
|
74
|
+
changefreq?: ReactionarySitemapChangeFrequency;
|
|
75
|
+
priority?: number;
|
|
76
|
+
}
|
|
77
|
+
export interface ReactionaryProductSitemapDefinition extends Partial<ReactionarySitemapBaseDefinition> {
|
|
78
|
+
type: 'products';
|
|
79
|
+
feed: string;
|
|
80
|
+
}
|
|
81
|
+
export interface ReactionaryCategorySitemapDefinition extends ReactionarySitemapBaseDefinition {
|
|
82
|
+
type: 'categories';
|
|
83
|
+
urlTemplate: string;
|
|
84
|
+
pageSize?: number;
|
|
85
|
+
maxPagesPerLevel?: number;
|
|
86
|
+
maxDepth?: number;
|
|
87
|
+
includeChildren?: boolean;
|
|
88
|
+
}
|
|
89
|
+
export interface ReactionaryStoreSitemapDefinition extends ReactionarySitemapBaseDefinition {
|
|
90
|
+
type: 'stores';
|
|
91
|
+
urlTemplate: string;
|
|
92
|
+
proximity: {
|
|
93
|
+
longitude: number;
|
|
94
|
+
latitude: number;
|
|
95
|
+
distance: number;
|
|
96
|
+
limit: number;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export type ReactionarySitemapDefinition = ReactionaryProductSitemapDefinition | ReactionaryCategorySitemapDefinition | ReactionaryStoreSitemapDefinition;
|
|
100
|
+
export interface ReactionarySitemapOptions {
|
|
101
|
+
baseUrl: string;
|
|
102
|
+
sources: Record<string, ReactionarySitemapDefinition>;
|
|
103
|
+
include?: string[];
|
|
104
|
+
}
|
|
105
|
+
export interface ReactionaryFeedImage {
|
|
106
|
+
url: string;
|
|
107
|
+
altText?: string;
|
|
108
|
+
}
|
|
109
|
+
export interface ReactionaryFeedMoney {
|
|
110
|
+
value: number;
|
|
111
|
+
currency: string;
|
|
112
|
+
}
|
|
113
|
+
export interface ReactionaryFeedBarcode {
|
|
114
|
+
type: 'ean' | 'gtin' | 'upc' | 'barcode';
|
|
115
|
+
value: string;
|
|
116
|
+
}
|
|
117
|
+
export interface ReactionaryFeedVariantOption {
|
|
118
|
+
name: string;
|
|
119
|
+
value: string;
|
|
120
|
+
}
|
|
121
|
+
export interface ReactionaryFeedAvailability {
|
|
122
|
+
available: boolean;
|
|
123
|
+
status: 'in_stock' | 'out_of_stock' | 'backorder' | 'preorder' | 'discontinued';
|
|
124
|
+
quantity?: number;
|
|
125
|
+
}
|
|
126
|
+
export interface ReactionaryFeedCategory {
|
|
127
|
+
id: string;
|
|
128
|
+
name: string;
|
|
129
|
+
slug?: string;
|
|
130
|
+
}
|
|
131
|
+
export interface ReactionaryFeedRatingSummary {
|
|
132
|
+
averageRating: number;
|
|
133
|
+
totalRatings?: number;
|
|
134
|
+
ratingDistribution?: Record<string, number>;
|
|
135
|
+
}
|
|
136
|
+
export interface ReactionaryFeedReview {
|
|
137
|
+
id: string;
|
|
138
|
+
authorName: string;
|
|
139
|
+
rating: number;
|
|
140
|
+
title: string;
|
|
141
|
+
content: string;
|
|
142
|
+
createdAt: string;
|
|
143
|
+
verified: boolean;
|
|
144
|
+
}
|
|
145
|
+
export interface ReactionaryFeedVariant {
|
|
146
|
+
id: string;
|
|
147
|
+
title: string;
|
|
148
|
+
description?: string;
|
|
149
|
+
url?: string;
|
|
150
|
+
ean?: string;
|
|
151
|
+
gtin?: string;
|
|
152
|
+
upc?: string;
|
|
153
|
+
barcode?: string;
|
|
154
|
+
manufacturerPartNumber?: string;
|
|
155
|
+
images: ReactionaryFeedImage[];
|
|
156
|
+
price?: ReactionaryFeedMoney;
|
|
157
|
+
listPrice?: ReactionaryFeedMoney;
|
|
158
|
+
availability?: ReactionaryFeedAvailability;
|
|
159
|
+
barcodes: ReactionaryFeedBarcode[];
|
|
160
|
+
options: ReactionaryFeedVariantOption[];
|
|
161
|
+
metadata?: Record<string, unknown>;
|
|
162
|
+
}
|
|
163
|
+
export interface ReactionaryFeedProduct {
|
|
164
|
+
id: string;
|
|
165
|
+
title: string;
|
|
166
|
+
description?: string;
|
|
167
|
+
url?: string;
|
|
168
|
+
brand?: string;
|
|
169
|
+
manufacturer?: string;
|
|
170
|
+
categoryPath?: ReactionaryFeedCategory[];
|
|
171
|
+
ratingSummary?: ReactionaryFeedRatingSummary;
|
|
172
|
+
reviews?: ReactionaryFeedReview[];
|
|
173
|
+
images: ReactionaryFeedImage[];
|
|
174
|
+
variants: ReactionaryFeedVariant[];
|
|
175
|
+
metadata?: Record<string, unknown>;
|
|
176
|
+
}
|
|
177
|
+
export interface ReactionaryFeedGenerateInput {
|
|
178
|
+
feedId: string;
|
|
179
|
+
feed: ReactionaryFeedDefinition;
|
|
180
|
+
requestContext: RequestContext;
|
|
181
|
+
}
|
|
182
|
+
export declare function assertReactionaryFeedClient(client: ReactionaryFeedClient): asserts client is ValidatedReactionaryFeedClient;
|
|
183
|
+
export declare function getMissingFeedClientOperations(client: ReactionaryFeedClient): string[];
|
|
184
|
+
export declare function getMissingSitemapClientOperations(client: ReactionaryFeedClient, source: ReactionarySitemapDefinition): string[];
|
|
185
|
+
export declare function assertReactionarySitemapClient(client: ReactionaryFeedClient, source: ReactionarySitemapDefinition): void;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { RequestContext } from '@reactionary/core';
|
|
2
|
+
import type { ReactionaryFeedClient, ReactionaryFeedDefinition, ReactionaryFeedGeneratorOptions, ReactionarySitemapDefinition, ReactionarySitemapEntry } from './feed-types.js';
|
|
3
|
+
export declare class ReactionarySitemapGenerator {
|
|
4
|
+
private readonly client;
|
|
5
|
+
private readonly feeds;
|
|
6
|
+
private readonly options;
|
|
7
|
+
constructor(client: ReactionaryFeedClient, feeds: Record<string, ReactionaryFeedDefinition>, options?: ReactionaryFeedGeneratorOptions);
|
|
8
|
+
entries(source: ReactionarySitemapDefinition, requestContext: RequestContext): AsyncGenerator<ReactionarySitemapEntry>;
|
|
9
|
+
private productEntries;
|
|
10
|
+
private categoryEntries;
|
|
11
|
+
private categoryWithChildrenEntries;
|
|
12
|
+
private iterCategoryPageResults;
|
|
13
|
+
private storeEntries;
|
|
14
|
+
}
|
|
15
|
+
export declare function toTemplatedUrl(urlTemplate: string, languageContext: {
|
|
16
|
+
locale: string;
|
|
17
|
+
}, values: {
|
|
18
|
+
id: string;
|
|
19
|
+
slug?: string;
|
|
20
|
+
}): string | undefined;
|