@reactionary/magento 0.0.1 → 0.10.19
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/LICENSE +21 -0
- package/capabilities/cart.capability.js +22 -22
- package/capabilities/category.capability.js +10 -9
- package/capabilities/checkout.capability.js +308 -0
- package/capabilities/identity.capability.js +31 -16
- package/capabilities/inventory.capability.js +2 -2
- package/capabilities/order-search.capability.js +150 -0
- package/capabilities/order.capability.js +51 -0
- package/capabilities/price.capability.js +2 -2
- package/capabilities/product-associations.capability.js +118 -0
- package/capabilities/product-recommendations.capability.js +148 -0
- package/capabilities/product-reviews.capability.js +182 -0
- package/capabilities/product-search.capability.js +2 -2
- package/capabilities/product.capability.js +6 -5
- package/capabilities/profile.capability.js +283 -0
- package/core/client.js +324 -24
- package/core/initialize.js +150 -2
- package/core/product-reviews.graphql.js +72 -0
- package/factories/category/category.factory.js +1 -1
- package/factories/checkout/checkout.factory.js +203 -0
- package/factories/index.js +6 -0
- package/factories/order/order.factory.js +115 -0
- package/factories/order-search/order-search.factory.js +79 -0
- package/factories/product/product.factory.js +23 -18
- package/factories/product-associations/product-associations.factory.js +25 -0
- package/factories/product-reviews/product-reviews.factory.js +65 -0
- package/factories/product-search/product-search.factory.js +3 -4
- package/factories/profile/profile.factory.js +61 -0
- package/index.js +9 -0
- package/package.json +11 -6
- package/schema/capabilities.schema.js +16 -2
- package/schema/configuration.schema.js +5 -1
- package/src/capabilities/cart.capability.d.ts +5 -0
- package/src/capabilities/checkout.capability.d.ts +30 -0
- package/src/capabilities/order-search.capability.d.ts +16 -0
- package/src/capabilities/order.capability.d.ts +12 -0
- package/src/capabilities/product-associations.capability.d.ts +23 -0
- package/src/capabilities/product-recommendations.capability.d.ts +24 -0
- package/src/capabilities/product-reviews.capability.d.ts +35 -0
- package/src/capabilities/profile.capability.d.ts +24 -0
- package/src/core/client.d.ts +77 -2
- package/src/core/initialize.types.d.ts +40 -7
- package/src/core/product-reviews.graphql.d.ts +10 -0
- package/src/factories/category/category.factory.d.ts +2 -2
- package/src/factories/checkout/checkout.factory.d.ts +32 -0
- package/src/factories/index.d.ts +6 -0
- package/src/factories/order/order.factory.d.ts +77 -0
- package/src/factories/order-search/order-search.factory.d.ts +43 -0
- package/src/factories/product/product.factory.d.ts +2 -1
- package/src/factories/product-associations/product-associations.factory.d.ts +15 -0
- package/src/factories/product-reviews/product-reviews.factory.d.ts +40 -0
- package/src/factories/profile/profile.factory.d.ts +43 -0
- package/src/index.d.ts +9 -0
- package/src/schema/capabilities.schema.d.ts +50 -2
- package/src/schema/configuration.schema.d.ts +7 -0
- package/src/schema/magento.types.d.ts +219 -0
- package/src/utils/magento-product-lookup.d.ts +15 -0
- package/src/utils/magento-product.d.ts +36 -0
- package/src/utils/magento-review.d.ts +20 -0
- package/utils/magento-product-lookup.js +76 -0
- package/utils/magento-product.js +110 -0
- package/utils/magento-review.js +34 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type Image, type ProductSearchResultItem, type ProductSearchResultItemVariant } from '@reactionary/core';
|
|
2
|
+
import type { MagentoConfiguration } from '../schema/configuration.schema.js';
|
|
3
|
+
import type { MagentoProduct } from '../schema/magento.types.js';
|
|
4
|
+
export declare function getCustomAttribute(product: MagentoProduct, code: string): string | undefined;
|
|
5
|
+
/**
|
|
6
|
+
* The `external_id` carries the identifier the surrounding commerce landscape
|
|
7
|
+
* uses. Depending on how it was added to Magento it surfaces either as an
|
|
8
|
+
* extension attribute or as a plain EAV (custom) attribute, so both are checked.
|
|
9
|
+
*/
|
|
10
|
+
export declare function getProductExternalId(product: MagentoProduct): string | undefined;
|
|
11
|
+
export declare function getProductKey(product: MagentoProduct): string;
|
|
12
|
+
/**
|
|
13
|
+
* `original_dam_reference` holds the PIM-sorted DAM imagery. Order is
|
|
14
|
+
* significant: the first entry is the primary image.
|
|
15
|
+
*
|
|
16
|
+
* Expected attribute value:
|
|
17
|
+
* ```json
|
|
18
|
+
* {
|
|
19
|
+
* "images": [
|
|
20
|
+
* {
|
|
21
|
+
* "url": "https://dam.example/a.jpg",
|
|
22
|
+
* "altText": "Front view",
|
|
23
|
+
* "dimensions": { "width": 400, "height": 600 }
|
|
24
|
+
* }
|
|
25
|
+
* ]
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseDamImages(raw: string | undefined, fallbackAltText: string): Image[];
|
|
30
|
+
export declare function buildMagentoImageUrl(config: MagentoConfiguration, file: string): string;
|
|
31
|
+
export declare function buildProductSearchResultItemVariant(config: MagentoConfiguration, variant: MagentoProduct, product: MagentoProduct): ProductSearchResultItemVariant;
|
|
32
|
+
/**
|
|
33
|
+
* Builds the search-result representation used by the association and
|
|
34
|
+
* recommendation capabilities, which key products by their `external_id`.
|
|
35
|
+
*/
|
|
36
|
+
export declare function buildProductSearchResultItem(config: MagentoConfiguration, product: MagentoProduct): ProductSearchResultItem;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { MagentoProductReview } from '../schema/magento.types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Magento expresses ratings as a percentage (0-100) — `rating_summary` on the
|
|
4
|
+
* product and `average_rating` on the individual review — while reactionary
|
|
5
|
+
* normalizes everything onto a 0-5 star scale.
|
|
6
|
+
*/
|
|
7
|
+
export declare function ratingPercentToStars(percent: number | null | undefined): number;
|
|
8
|
+
/**
|
|
9
|
+
* Magento emits timestamps as MySQL datetimes in UTC ("2024-05-04 12:00:00"),
|
|
10
|
+
* whereas the reactionary models expose ISO8601. Unparseable values are passed
|
|
11
|
+
* through rather than replaced with a fabricated date.
|
|
12
|
+
*/
|
|
13
|
+
export declare function toIsoTimestamp(value: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Magento's GraphQL review payload carries no identifier of its own, so the key
|
|
16
|
+
* is derived from the fields that together identify a review. Deriving it
|
|
17
|
+
* deterministically (rather than from the position in the page) keeps the key
|
|
18
|
+
* stable across pages, requests and cache entries.
|
|
19
|
+
*/
|
|
20
|
+
export declare function buildProductReviewKey(sku: string, review: MagentoProductReview): string;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
function singleFilter(field, value, conditionType = "eq") {
|
|
2
|
+
const params = new URLSearchParams();
|
|
3
|
+
params.set("searchCriteria[filterGroups][0][filters][0][field]", field);
|
|
4
|
+
params.set("searchCriteria[filterGroups][0][filters][0][value]", value);
|
|
5
|
+
params.set(
|
|
6
|
+
"searchCriteria[filterGroups][0][filters][0][condition_type]",
|
|
7
|
+
conditionType
|
|
8
|
+
);
|
|
9
|
+
return params;
|
|
10
|
+
}
|
|
11
|
+
async function resolveProductSku(magentoApi, key) {
|
|
12
|
+
const byExternalId = singleFilter("external_id", key);
|
|
13
|
+
byExternalId.set("searchCriteria[pageSize]", "1");
|
|
14
|
+
try {
|
|
15
|
+
const result = await magentoApi.searchProducts(byExternalId);
|
|
16
|
+
const product = result.items?.[0];
|
|
17
|
+
if (product?.sku) {
|
|
18
|
+
return product.sku;
|
|
19
|
+
}
|
|
20
|
+
} catch {
|
|
21
|
+
}
|
|
22
|
+
if (/^\d+$/.test(key)) {
|
|
23
|
+
const byEntityId = singleFilter("entity_id", key);
|
|
24
|
+
byEntityId.set("searchCriteria[pageSize]", "1");
|
|
25
|
+
try {
|
|
26
|
+
const result = await magentoApi.searchProducts(byEntityId);
|
|
27
|
+
const product = result.items?.[0];
|
|
28
|
+
if (product?.sku) {
|
|
29
|
+
return product.sku;
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return key;
|
|
36
|
+
}
|
|
37
|
+
async function fetchProductsBySkus(magentoApi, skus) {
|
|
38
|
+
if (skus.length === 0) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
const params = singleFilter("sku", skus.join(","), "in");
|
|
42
|
+
params.set("searchCriteria[pageSize]", String(skus.length));
|
|
43
|
+
const result = await magentoApi.searchProducts(params);
|
|
44
|
+
const found = result.items ?? [];
|
|
45
|
+
const bySku = new Map(found.map((product) => [product.sku, product]));
|
|
46
|
+
return skus.map((sku) => bySku.get(sku)).filter((product) => product !== void 0);
|
|
47
|
+
}
|
|
48
|
+
async function fetchProductsInCategory(magentoApi, categoryId, limit) {
|
|
49
|
+
const params = singleFilter("category_id", categoryId);
|
|
50
|
+
params.set("searchCriteria[pageSize]", String(limit));
|
|
51
|
+
params.set("searchCriteria[currentPage]", "1");
|
|
52
|
+
const result = await magentoApi.searchProducts(params);
|
|
53
|
+
return result.items ?? [];
|
|
54
|
+
}
|
|
55
|
+
async function findCategoryByName(magentoApi, collectionName) {
|
|
56
|
+
const client = await magentoApi.getClient();
|
|
57
|
+
for (const field of ["external_id", "url_key", "name"]) {
|
|
58
|
+
const params = singleFilter(field, collectionName);
|
|
59
|
+
params.set("searchCriteria[pageSize]", "1");
|
|
60
|
+
try {
|
|
61
|
+
const response = await client.store.category.list(params);
|
|
62
|
+
const category = response.items?.[0];
|
|
63
|
+
if (category) {
|
|
64
|
+
return category;
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
export {
|
|
72
|
+
fetchProductsBySkus,
|
|
73
|
+
fetchProductsInCategory,
|
|
74
|
+
findCategoryByName,
|
|
75
|
+
resolveProductSku
|
|
76
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ImageSchema,
|
|
3
|
+
ProductSearchResultItemVariantSchema,
|
|
4
|
+
ProductVariantIdentifierSchema
|
|
5
|
+
} from "@reactionary/core";
|
|
6
|
+
import createDebug from "debug";
|
|
7
|
+
const debug = createDebug("reactionary:magento:product-media");
|
|
8
|
+
function getCustomAttribute(product, code) {
|
|
9
|
+
if (!product.custom_attributes) return void 0;
|
|
10
|
+
const found = product.custom_attributes.find((a) => a.attribute_code === code);
|
|
11
|
+
if (found?.value === null || found?.value === void 0) return void 0;
|
|
12
|
+
return String(found.value);
|
|
13
|
+
}
|
|
14
|
+
function getProductExternalId(product) {
|
|
15
|
+
const fromExtension = product.extension_attributes?.["external_id"];
|
|
16
|
+
if (typeof fromExtension === "string" || typeof fromExtension === "number") {
|
|
17
|
+
return String(fromExtension);
|
|
18
|
+
}
|
|
19
|
+
return getCustomAttribute(product, "external_id");
|
|
20
|
+
}
|
|
21
|
+
function getProductKey(product) {
|
|
22
|
+
return getProductExternalId(product) ?? (product.id !== void 0 ? String(product.id) : product.sku);
|
|
23
|
+
}
|
|
24
|
+
function parseDamImages(raw, fallbackAltText) {
|
|
25
|
+
const trimmed = (raw ?? "").trim();
|
|
26
|
+
if (trimmed.length === 0) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
let parsed;
|
|
30
|
+
try {
|
|
31
|
+
parsed = JSON.parse(trimmed);
|
|
32
|
+
} catch {
|
|
33
|
+
debug("original_dam_reference is not valid JSON: %s", trimmed);
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
if (!isRecord(parsed) || !Array.isArray(parsed["images"])) {
|
|
37
|
+
debug('original_dam_reference has no "images" array: %s', trimmed);
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
return parsed["images"].map((entry) => parseDamImage(entry, fallbackAltText)).filter((image) => image !== null);
|
|
41
|
+
}
|
|
42
|
+
function isRecord(value) {
|
|
43
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
44
|
+
}
|
|
45
|
+
function toPixelSize(value) {
|
|
46
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
47
|
+
}
|
|
48
|
+
function parseDamImage(entry, fallbackAltText) {
|
|
49
|
+
if (!isRecord(entry)) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const url = entry["url"];
|
|
53
|
+
if (typeof url !== "string" || url.trim().length === 0) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const altText = entry["altText"];
|
|
57
|
+
const dimensions = isRecord(entry["dimensions"]) ? entry["dimensions"] : void 0;
|
|
58
|
+
return ImageSchema.parse({
|
|
59
|
+
sourceUrl: url.trim(),
|
|
60
|
+
altText: typeof altText === "string" && altText.trim().length > 0 ? altText.trim() : fallbackAltText,
|
|
61
|
+
width: toPixelSize(dimensions?.["width"]),
|
|
62
|
+
height: toPixelSize(dimensions?.["height"])
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function normalizeBaseUrl(baseUrl) {
|
|
66
|
+
return baseUrl.replace(/\/+$/, "");
|
|
67
|
+
}
|
|
68
|
+
function buildMagentoImageUrl(config, file) {
|
|
69
|
+
const mediaUrl = config.mediaUrl;
|
|
70
|
+
if (mediaUrl) {
|
|
71
|
+
return `${mediaUrl.replace(/\/+$/, "")}${file.startsWith("/") ? "" : "/"}${file}`;
|
|
72
|
+
}
|
|
73
|
+
const storeBase = normalizeBaseUrl(config.baseUrl);
|
|
74
|
+
return `${storeBase}/media/catalog/product${file.startsWith("/") ? "" : "/"}${file}`;
|
|
75
|
+
}
|
|
76
|
+
function buildProductSearchResultItemVariant(config, variant, product) {
|
|
77
|
+
const media = product.media_gallery_entries;
|
|
78
|
+
const firstImage = media && media.length > 0 ? media[0].file : null;
|
|
79
|
+
const img = ImageSchema.parse({
|
|
80
|
+
sourceUrl: firstImage ? buildMagentoImageUrl(config, firstImage) : "",
|
|
81
|
+
altText: product.name || void 0
|
|
82
|
+
});
|
|
83
|
+
return ProductSearchResultItemVariantSchema.parse({
|
|
84
|
+
variant: ProductVariantIdentifierSchema.parse({
|
|
85
|
+
sku: variant.sku
|
|
86
|
+
}),
|
|
87
|
+
image: img
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function buildProductSearchResultItem(config, product) {
|
|
91
|
+
const variants = [];
|
|
92
|
+
if (product.sku) {
|
|
93
|
+
variants.push(buildProductSearchResultItemVariant(config, product, product));
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
identifier: { key: getProductKey(product) },
|
|
97
|
+
name: product.name || product.sku,
|
|
98
|
+
slug: getCustomAttribute(product, "url_key") ?? getCustomAttribute(product, "url_path") ?? "",
|
|
99
|
+
variants
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export {
|
|
103
|
+
buildMagentoImageUrl,
|
|
104
|
+
buildProductSearchResultItem,
|
|
105
|
+
buildProductSearchResultItemVariant,
|
|
106
|
+
getCustomAttribute,
|
|
107
|
+
getProductExternalId,
|
|
108
|
+
getProductKey,
|
|
109
|
+
parseDamImages
|
|
110
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
function ratingPercentToStars(percent) {
|
|
2
|
+
if (percent === null || percent === void 0 || Number.isNaN(percent)) {
|
|
3
|
+
return 0;
|
|
4
|
+
}
|
|
5
|
+
const stars = percent / 100 * 5;
|
|
6
|
+
return Math.min(5, Math.max(0, Math.round(stars * 100) / 100));
|
|
7
|
+
}
|
|
8
|
+
function toIsoTimestamp(value) {
|
|
9
|
+
const normalized = value.includes("T") ? value : `${value.replace(" ", "T")}Z`;
|
|
10
|
+
const parsed = new Date(normalized);
|
|
11
|
+
return Number.isNaN(parsed.getTime()) ? value : parsed.toISOString();
|
|
12
|
+
}
|
|
13
|
+
function hashToken(value) {
|
|
14
|
+
let hash = 5381;
|
|
15
|
+
for (let i = 0; i < value.length; i++) {
|
|
16
|
+
hash = (hash << 5) + hash + value.charCodeAt(i) | 0;
|
|
17
|
+
}
|
|
18
|
+
return (hash >>> 0).toString(36);
|
|
19
|
+
}
|
|
20
|
+
function buildProductReviewKey(sku, review) {
|
|
21
|
+
const source = [
|
|
22
|
+
sku,
|
|
23
|
+
review.created_at,
|
|
24
|
+
review.nickname,
|
|
25
|
+
review.summary,
|
|
26
|
+
review.text
|
|
27
|
+
].join("|");
|
|
28
|
+
return `${sku}-${hashToken(source)}`;
|
|
29
|
+
}
|
|
30
|
+
export {
|
|
31
|
+
buildProductReviewKey,
|
|
32
|
+
ratingPercentToStars,
|
|
33
|
+
toIsoTimestamp
|
|
34
|
+
};
|