@reactionary/algolia 0.6.3
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 +11 -0
- package/capabilities/analytics.capability.js +77 -0
- package/capabilities/index.js +3 -0
- package/capabilities/product-recommendations.capability.js +195 -0
- package/capabilities/product-search.capability.js +94 -0
- package/core/initialize.js +67 -0
- package/core/initialize.types.js +13 -0
- package/factories/index.js +1 -0
- package/factories/product-search/product-search.factory.js +124 -0
- package/index.js +5 -0
- package/package.json +15 -0
- package/schema/capabilities.schema.js +23 -0
- package/schema/configuration.schema.js +9 -0
- package/schema/index.js +3 -0
- package/schema/product-recommendation.schema.js +9 -0
- package/schema/search.schema.js +13 -0
- package/src/capabilities/analytics.capability.d.ts +12 -0
- package/src/capabilities/index.d.ts +3 -0
- package/src/capabilities/product-recommendations.capability.d.ts +103 -0
- package/src/capabilities/product-search.capability.d.ts +21 -0
- package/src/core/initialize.d.ts +5 -0
- package/src/core/initialize.types.d.ts +43 -0
- package/src/factories/index.d.ts +1 -0
- package/src/factories/product-search/product-search.factory.d.ts +16 -0
- package/src/index.d.ts +5 -0
- package/src/schema/capabilities.schema.d.ts +44 -0
- package/src/schema/configuration.schema.d.ts +7 -0
- package/src/schema/index.d.ts +3 -0
- package/src/schema/product-recommendation.schema.d.ts +8 -0
- package/src/schema/search.schema.d.ts +113 -0
- package/src/test/client-builder-product-search-extension.example.d.ts +1 -0
- package/test/client-builder-product-search-extension.example.js +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AnalyticsCapability
|
|
3
|
+
} from "@reactionary/core";
|
|
4
|
+
import {
|
|
5
|
+
algoliasearch
|
|
6
|
+
} from "algoliasearch";
|
|
7
|
+
class AlgoliaAnalyticsCapability extends AnalyticsCapability {
|
|
8
|
+
client;
|
|
9
|
+
config;
|
|
10
|
+
constructor(cache, requestContext, config) {
|
|
11
|
+
super(cache, requestContext);
|
|
12
|
+
this.config = config;
|
|
13
|
+
this.client = algoliasearch(this.config.appId, this.config.apiKey).initInsights({});
|
|
14
|
+
}
|
|
15
|
+
async processProductAddToCart(event) {
|
|
16
|
+
if (event.source && event.source.type === "search") {
|
|
17
|
+
const algoliaEvent = {
|
|
18
|
+
eventName: "addToCart",
|
|
19
|
+
eventType: "conversion",
|
|
20
|
+
eventSubtype: "addToCart",
|
|
21
|
+
index: this.config.indexName,
|
|
22
|
+
objectIDs: [event.product.key],
|
|
23
|
+
userToken: this.context.session.identityContext.personalizationKey,
|
|
24
|
+
queryID: event.source.identifier.key
|
|
25
|
+
};
|
|
26
|
+
await this.client.pushEvents({
|
|
27
|
+
events: [algoliaEvent]
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async processProductSummaryClick(event) {
|
|
32
|
+
if (event.source && event.source.type === "search") {
|
|
33
|
+
const algoliaEvent = {
|
|
34
|
+
eventName: "click",
|
|
35
|
+
eventType: "click",
|
|
36
|
+
index: this.config.indexName,
|
|
37
|
+
objectIDs: [event.product.key],
|
|
38
|
+
userToken: this.context.session.identityContext.personalizationKey,
|
|
39
|
+
positions: [event.position],
|
|
40
|
+
queryID: event.source.identifier.key
|
|
41
|
+
};
|
|
42
|
+
await this.client.pushEvents({
|
|
43
|
+
events: [algoliaEvent]
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async processProductSummaryView(event) {
|
|
48
|
+
if (event.source && event.source.type === "search") {
|
|
49
|
+
const algoliaEvent = {
|
|
50
|
+
eventName: "view",
|
|
51
|
+
eventType: "view",
|
|
52
|
+
index: this.config.indexName,
|
|
53
|
+
objectIDs: event.products.map((x) => x.key),
|
|
54
|
+
userToken: this.context.session.identityContext.personalizationKey
|
|
55
|
+
};
|
|
56
|
+
await this.client.pushEvents({
|
|
57
|
+
events: [algoliaEvent]
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async processPurchase(event) {
|
|
62
|
+
const algoliaEvent = {
|
|
63
|
+
eventName: "purchase",
|
|
64
|
+
eventType: "conversion",
|
|
65
|
+
eventSubtype: "purchase",
|
|
66
|
+
index: this.config.indexName,
|
|
67
|
+
objectIDs: event.order.items.map((x) => x.variant.sku),
|
|
68
|
+
userToken: this.context.session.identityContext.personalizationKey
|
|
69
|
+
};
|
|
70
|
+
await this.client.pushEvents({
|
|
71
|
+
events: [algoliaEvent]
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export {
|
|
76
|
+
AlgoliaAnalyticsCapability
|
|
77
|
+
};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ProductRecommendationsCapability,
|
|
3
|
+
ProductSearchResultItemVariantSchema,
|
|
4
|
+
ImageSchema
|
|
5
|
+
} from "@reactionary/core";
|
|
6
|
+
import {
|
|
7
|
+
liteClient
|
|
8
|
+
} from "algoliasearch/lite";
|
|
9
|
+
class AlgoliaProductRecommendationsCapability extends ProductRecommendationsCapability {
|
|
10
|
+
config;
|
|
11
|
+
client;
|
|
12
|
+
constructor(config, cache, context) {
|
|
13
|
+
super(cache, context);
|
|
14
|
+
this.config = config;
|
|
15
|
+
this.client = liteClient(this.config.appId, this.config.apiKey);
|
|
16
|
+
}
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
18
|
+
getRecommendationThreshold(_algorithm) {
|
|
19
|
+
return 10;
|
|
20
|
+
}
|
|
21
|
+
getQueryParametersForRecommendations(algorithm) {
|
|
22
|
+
return {
|
|
23
|
+
userToken: this.context.session.identityContext?.personalizationKey || "anonymous",
|
|
24
|
+
analytics: true,
|
|
25
|
+
analyticsTags: ["reactionary", algorithm],
|
|
26
|
+
clickAnalytics: true
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Get frequently bought together recommendations using Algolia Recommend
|
|
31
|
+
*/
|
|
32
|
+
async getFrequentlyBoughtTogetherRecommendations(query) {
|
|
33
|
+
try {
|
|
34
|
+
const response = await this.client.getRecommendations({
|
|
35
|
+
requests: [
|
|
36
|
+
{
|
|
37
|
+
indexName: this.config.indexName,
|
|
38
|
+
model: "bought-together",
|
|
39
|
+
objectID: query.sourceProduct.key,
|
|
40
|
+
maxRecommendations: query.numberOfRecommendations,
|
|
41
|
+
threshold: this.getRecommendationThreshold("bought-together"),
|
|
42
|
+
queryParameters: this.getQueryParametersForRecommendations("bought-together")
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
});
|
|
46
|
+
const result = [];
|
|
47
|
+
if (response.results) {
|
|
48
|
+
for (const res of response.results) {
|
|
49
|
+
result.push(...this.parseRecommendation(res, query));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error("Error fetching frequently bought together recommendations:", error);
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Get similar product recommendations using Algolia Recommend
|
|
60
|
+
*/
|
|
61
|
+
async getSimilarProductsRecommendations(query) {
|
|
62
|
+
try {
|
|
63
|
+
const response = await this.client.getRecommendations({
|
|
64
|
+
requests: [
|
|
65
|
+
{
|
|
66
|
+
indexName: this.config.indexName,
|
|
67
|
+
model: "looking-similar",
|
|
68
|
+
objectID: query.sourceProduct.key,
|
|
69
|
+
maxRecommendations: query.numberOfRecommendations,
|
|
70
|
+
threshold: this.getRecommendationThreshold("looking-similar"),
|
|
71
|
+
queryParameters: this.getQueryParametersForRecommendations("looking-similar")
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
});
|
|
75
|
+
const result = [];
|
|
76
|
+
if (response.results) {
|
|
77
|
+
for (const res of response.results) {
|
|
78
|
+
result.push(...this.parseRecommendation(res, query));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error("Error fetching similar product recommendations:", error);
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Get related product recommendations using Algolia Recommend
|
|
89
|
+
*/
|
|
90
|
+
async getRelatedProductsRecommendations(query) {
|
|
91
|
+
try {
|
|
92
|
+
const response = await this.client.getRecommendations({
|
|
93
|
+
requests: [
|
|
94
|
+
{
|
|
95
|
+
indexName: this.config.indexName,
|
|
96
|
+
model: "related-products",
|
|
97
|
+
objectID: query.sourceProduct.key,
|
|
98
|
+
maxRecommendations: query.numberOfRecommendations,
|
|
99
|
+
threshold: this.getRecommendationThreshold("related-products"),
|
|
100
|
+
queryParameters: this.getQueryParametersForRecommendations("related-products")
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
});
|
|
104
|
+
const result = [];
|
|
105
|
+
if (response.results) {
|
|
106
|
+
for (const res of response.results) {
|
|
107
|
+
result.push(...this.parseRecommendation(res, query));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error("Error fetching related product recommendations:", error);
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Get trending in category recommendations using Algolia Recommend
|
|
118
|
+
*/
|
|
119
|
+
async getTrendingInCategoryRecommendations(query) {
|
|
120
|
+
try {
|
|
121
|
+
const response = await this.client.getRecommendations({
|
|
122
|
+
requests: [
|
|
123
|
+
{
|
|
124
|
+
indexName: this.config.indexName,
|
|
125
|
+
model: "trending-items",
|
|
126
|
+
facetName: "categories",
|
|
127
|
+
facetValue: query.sourceCategory.key,
|
|
128
|
+
maxRecommendations: query.numberOfRecommendations,
|
|
129
|
+
threshold: this.getRecommendationThreshold("trending-items"),
|
|
130
|
+
queryParameters: this.getQueryParametersForRecommendations("trending-items")
|
|
131
|
+
}
|
|
132
|
+
]
|
|
133
|
+
});
|
|
134
|
+
const result = [];
|
|
135
|
+
if (response.results) {
|
|
136
|
+
for (const res of response.results) {
|
|
137
|
+
result.push(...this.parseRecommendation(res, query));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return result;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error("Error fetching trending in category recommendations:", error);
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
parseRecommendation(res, query) {
|
|
147
|
+
const result = [];
|
|
148
|
+
for (const hit of res.hits) {
|
|
149
|
+
const recommendationIdentifier = {
|
|
150
|
+
key: res.queryID || "x",
|
|
151
|
+
algorithm: query.algorithm,
|
|
152
|
+
abTestID: res.abTestID,
|
|
153
|
+
abTestVariantID: res.abTestVariantID
|
|
154
|
+
};
|
|
155
|
+
const recommendation = this.parseSingle(hit, recommendationIdentifier);
|
|
156
|
+
result.push(recommendation);
|
|
157
|
+
}
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Maps Algolia recommendation results to ProductRecommendation format
|
|
162
|
+
*/
|
|
163
|
+
parseSingle(hit, recommendationIdentifier) {
|
|
164
|
+
const product = this.parseSearchResultItem(hit);
|
|
165
|
+
return {
|
|
166
|
+
recommendationIdentifier,
|
|
167
|
+
recommendationReturnType: "productSearchResultItem",
|
|
168
|
+
product
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
parseSearchResultItem(body) {
|
|
172
|
+
const product = {
|
|
173
|
+
identifier: { key: body.objectID },
|
|
174
|
+
name: body.name || body.objectID,
|
|
175
|
+
slug: body.slug || body.objectID,
|
|
176
|
+
variants: [...body.variants || []].map((variant) => this.parseVariant(variant, body))
|
|
177
|
+
};
|
|
178
|
+
return product;
|
|
179
|
+
}
|
|
180
|
+
parseVariant(variant, product) {
|
|
181
|
+
const result = ProductSearchResultItemVariantSchema.parse({
|
|
182
|
+
variant: {
|
|
183
|
+
sku: variant.sku
|
|
184
|
+
},
|
|
185
|
+
image: ImageSchema.parse({
|
|
186
|
+
sourceUrl: variant.image,
|
|
187
|
+
altText: product.name || ""
|
|
188
|
+
})
|
|
189
|
+
});
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
export {
|
|
194
|
+
AlgoliaProductRecommendationsCapability
|
|
195
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
4
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
5
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
6
|
+
if (decorator = decorators[i])
|
|
7
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
8
|
+
if (kind && result)
|
|
9
|
+
__defProp(target, key, result);
|
|
10
|
+
return result;
|
|
11
|
+
};
|
|
12
|
+
import {
|
|
13
|
+
FacetIdentifierSchema,
|
|
14
|
+
FacetValueIdentifierSchema,
|
|
15
|
+
ProductSearchCapability,
|
|
16
|
+
ProductSearchQueryByTermSchema,
|
|
17
|
+
ProductSearchResultSchema,
|
|
18
|
+
Reactionary,
|
|
19
|
+
success
|
|
20
|
+
} from "@reactionary/core";
|
|
21
|
+
import { algoliasearch } from "algoliasearch";
|
|
22
|
+
class AlgoliaProductSearchCapability extends ProductSearchCapability {
|
|
23
|
+
config;
|
|
24
|
+
factory;
|
|
25
|
+
constructor(cache, context, config, factory) {
|
|
26
|
+
super(cache, context);
|
|
27
|
+
this.config = config;
|
|
28
|
+
this.factory = factory;
|
|
29
|
+
}
|
|
30
|
+
queryByTermPayload(payload) {
|
|
31
|
+
const facetsThatAreNotCategory = payload.search.facets.filter(
|
|
32
|
+
(x) => x.facet.key !== "categories"
|
|
33
|
+
);
|
|
34
|
+
const categoryFacet = payload.search.facets.find((x) => x.facet.key === "categories") || payload.search.categoryFilter;
|
|
35
|
+
const finalFilters = [...payload.search.filters || []];
|
|
36
|
+
const finalFacetFilters = [
|
|
37
|
+
...facetsThatAreNotCategory.map((x) => `${x.facet.key}:${x.key}`)
|
|
38
|
+
];
|
|
39
|
+
if (categoryFacet) {
|
|
40
|
+
finalFilters.push(`categories:"${categoryFacet.key}"`);
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
indexName: this.config.indexName,
|
|
44
|
+
query: payload.search.term,
|
|
45
|
+
page: payload.search.paginationOptions.pageNumber - 1,
|
|
46
|
+
hitsPerPage: payload.search.paginationOptions.pageSize,
|
|
47
|
+
facets: ["*"],
|
|
48
|
+
analytics: true,
|
|
49
|
+
clickAnalytics: true,
|
|
50
|
+
facetFilters: finalFacetFilters,
|
|
51
|
+
filters: finalFilters.join(" AND ")
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async queryByTerm(payload) {
|
|
55
|
+
const client = algoliasearch(this.config.appId, this.config.apiKey);
|
|
56
|
+
const remote = await client.search({
|
|
57
|
+
requests: [this.queryByTermPayload(payload)]
|
|
58
|
+
});
|
|
59
|
+
const input = remote.results[0];
|
|
60
|
+
const result = this.factory.parseSearchResult(this.context, input, payload);
|
|
61
|
+
for (const selectedFacet of payload.search.facets) {
|
|
62
|
+
const facet = result.facets.find(
|
|
63
|
+
(f) => f.identifier.key === selectedFacet.facet.key
|
|
64
|
+
);
|
|
65
|
+
if (!facet) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const value = facet.values.find((v) => v.identifier.key === selectedFacet.key);
|
|
69
|
+
if (value) {
|
|
70
|
+
value.active = true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return success(result);
|
|
74
|
+
}
|
|
75
|
+
async createCategoryNavigationFilter(payload) {
|
|
76
|
+
const facetIdentifier = FacetIdentifierSchema.parse({
|
|
77
|
+
key: "categories"
|
|
78
|
+
});
|
|
79
|
+
const facetValueIdentifier = FacetValueIdentifierSchema.parse({
|
|
80
|
+
facet: facetIdentifier,
|
|
81
|
+
key: payload.categoryPath.map((c) => c.name).join(" > ")
|
|
82
|
+
});
|
|
83
|
+
return success(facetValueIdentifier);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
__decorateClass([
|
|
87
|
+
Reactionary({
|
|
88
|
+
inputSchema: ProductSearchQueryByTermSchema,
|
|
89
|
+
outputSchema: ProductSearchResultSchema
|
|
90
|
+
})
|
|
91
|
+
], AlgoliaProductSearchCapability.prototype, "queryByTerm", 1);
|
|
92
|
+
export {
|
|
93
|
+
AlgoliaProductSearchCapability
|
|
94
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { AlgoliaProductSearchCapability } from "../capabilities/product-search.capability.js";
|
|
2
|
+
import { AlgoliaAnalyticsCapability } from "../capabilities/analytics.capability.js";
|
|
3
|
+
import { AlgoliaProductRecommendationsCapability } from "../capabilities/product-recommendations.capability.js";
|
|
4
|
+
import { AlgoliaProductSearchFactory } from "../factories/product-search/product-search.factory.js";
|
|
5
|
+
import { AlgoliaProductSearchResultSchema } from "../schema/search.schema.js";
|
|
6
|
+
import {
|
|
7
|
+
resolveCapabilityWithFactory,
|
|
8
|
+
resolveDirectCapability
|
|
9
|
+
} from "./initialize.types.js";
|
|
10
|
+
function withAlgoliaCapabilities(configuration, capabilities) {
|
|
11
|
+
return (cache, context) => {
|
|
12
|
+
const client = {};
|
|
13
|
+
if (capabilities.productSearch?.enabled) {
|
|
14
|
+
const defaultFactory = new AlgoliaProductSearchFactory(
|
|
15
|
+
AlgoliaProductSearchResultSchema
|
|
16
|
+
);
|
|
17
|
+
client.productSearch = resolveCapabilityWithFactory(
|
|
18
|
+
capabilities.productSearch,
|
|
19
|
+
{
|
|
20
|
+
factory: defaultFactory,
|
|
21
|
+
capability: (args) => new AlgoliaProductSearchCapability(
|
|
22
|
+
args.cache,
|
|
23
|
+
args.context,
|
|
24
|
+
args.config,
|
|
25
|
+
args.factory
|
|
26
|
+
)
|
|
27
|
+
},
|
|
28
|
+
(factory) => ({
|
|
29
|
+
cache,
|
|
30
|
+
context,
|
|
31
|
+
config: configuration,
|
|
32
|
+
factory
|
|
33
|
+
})
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
if (capabilities.analytics?.enabled) {
|
|
37
|
+
client.analytics = resolveDirectCapability(
|
|
38
|
+
capabilities.analytics,
|
|
39
|
+
(args) => new AlgoliaAnalyticsCapability(args.cache, args.context, args.config),
|
|
40
|
+
{
|
|
41
|
+
cache,
|
|
42
|
+
context,
|
|
43
|
+
config: configuration
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
if (capabilities.productRecommendations?.enabled) {
|
|
48
|
+
client.productRecommendations = resolveDirectCapability(
|
|
49
|
+
capabilities.productRecommendations,
|
|
50
|
+
(args) => new AlgoliaProductRecommendationsCapability(
|
|
51
|
+
args.config,
|
|
52
|
+
args.cache,
|
|
53
|
+
args.context
|
|
54
|
+
),
|
|
55
|
+
{
|
|
56
|
+
cache,
|
|
57
|
+
context,
|
|
58
|
+
config: configuration
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return client;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export {
|
|
66
|
+
withAlgoliaCapabilities
|
|
67
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
function resolveCapabilityWithFactory(capability, defaults, buildCapabilityArgs) {
|
|
2
|
+
const factory = capability?.factory ?? defaults.factory;
|
|
3
|
+
const capabilityFactory = capability?.capability ?? defaults.capability;
|
|
4
|
+
return capabilityFactory(buildCapabilityArgs(factory));
|
|
5
|
+
}
|
|
6
|
+
function resolveDirectCapability(capability, defaultCapability, args) {
|
|
7
|
+
const capabilityFactory = capability?.capability ?? defaultCapability;
|
|
8
|
+
return capabilityFactory(args);
|
|
9
|
+
}
|
|
10
|
+
export {
|
|
11
|
+
resolveCapabilityWithFactory,
|
|
12
|
+
resolveDirectCapability
|
|
13
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./product-search/product-search.factory.js";
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FacetIdentifierSchema,
|
|
3
|
+
FacetValueIdentifierSchema,
|
|
4
|
+
ImageSchema,
|
|
5
|
+
ProductSearchResultFacetSchema,
|
|
6
|
+
ProductSearchResultFacetValueSchema,
|
|
7
|
+
ProductSearchResultItemVariantSchema
|
|
8
|
+
} from "@reactionary/core";
|
|
9
|
+
class AlgoliaProductSearchFactory {
|
|
10
|
+
productSearchResultSchema;
|
|
11
|
+
constructor(productSearchResultSchema) {
|
|
12
|
+
this.productSearchResultSchema = productSearchResultSchema;
|
|
13
|
+
}
|
|
14
|
+
parseSearchResult(_context, data, query) {
|
|
15
|
+
const body = this.parseInput(data);
|
|
16
|
+
const items = body.hits.map((hit) => this.parseSingle(hit));
|
|
17
|
+
let facets = [];
|
|
18
|
+
for (const id in body.facets) {
|
|
19
|
+
const values = body.facets[id];
|
|
20
|
+
const facetId = FacetIdentifierSchema.parse({ key: id });
|
|
21
|
+
facets.push(this.parseFacet(facetId, values));
|
|
22
|
+
}
|
|
23
|
+
const selectedCategoryFacet = query.search.facets.find((x) => x.facet.key === "categories") || query.search.categoryFilter;
|
|
24
|
+
let subCategoryFacet;
|
|
25
|
+
if (selectedCategoryFacet) {
|
|
26
|
+
const valueDepth = selectedCategoryFacet.key.split(" > ").length;
|
|
27
|
+
subCategoryFacet = facets.find(
|
|
28
|
+
(f) => f.identifier.key === `hierarchy.lvl${valueDepth}`
|
|
29
|
+
);
|
|
30
|
+
} else {
|
|
31
|
+
subCategoryFacet = facets.find((f) => f.identifier.key === "hierarchy.lvl0");
|
|
32
|
+
}
|
|
33
|
+
if (subCategoryFacet) {
|
|
34
|
+
subCategoryFacet.identifier = FacetIdentifierSchema.parse({ key: "categories" });
|
|
35
|
+
subCategoryFacet.name = "Categories";
|
|
36
|
+
for (const value of subCategoryFacet.values) {
|
|
37
|
+
const pathParts = value.identifier.key.split(" > ");
|
|
38
|
+
value.identifier.facet = subCategoryFacet.identifier;
|
|
39
|
+
value.name = pathParts[pathParts.length - 1];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
facets = facets.filter((f) => !f.identifier.key.startsWith("hierarchy.lvl"));
|
|
43
|
+
const result = {
|
|
44
|
+
identifier: {
|
|
45
|
+
term: query.search.term,
|
|
46
|
+
facets: query.search.facets,
|
|
47
|
+
filters: query.search.filters,
|
|
48
|
+
paginationOptions: query.search.paginationOptions,
|
|
49
|
+
index: body.index || "",
|
|
50
|
+
key: body.queryID || ""
|
|
51
|
+
},
|
|
52
|
+
pageNumber: (body.page || 0) + 1,
|
|
53
|
+
pageSize: body.hitsPerPage || 0,
|
|
54
|
+
totalCount: body.nbHits || 0,
|
|
55
|
+
totalPages: body.nbPages || 0,
|
|
56
|
+
items,
|
|
57
|
+
facets
|
|
58
|
+
};
|
|
59
|
+
return this.productSearchResultSchema.parse(result);
|
|
60
|
+
}
|
|
61
|
+
parseSingle(body) {
|
|
62
|
+
return {
|
|
63
|
+
identifier: { key: body.objectID },
|
|
64
|
+
name: body.name || body.objectID,
|
|
65
|
+
slug: body.slug || body.objectID,
|
|
66
|
+
variants: [...body.variants || []].map(
|
|
67
|
+
(variant) => this.parseVariant(variant, body)
|
|
68
|
+
)
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
parseVariant(variant, product) {
|
|
72
|
+
return ProductSearchResultItemVariantSchema.parse({
|
|
73
|
+
variant: {
|
|
74
|
+
sku: variant.sku
|
|
75
|
+
},
|
|
76
|
+
image: ImageSchema.parse({
|
|
77
|
+
sourceUrl: variant.image,
|
|
78
|
+
altText: product.name || ""
|
|
79
|
+
})
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
parseFacet(facetIdentifier, facetValues) {
|
|
83
|
+
const result = ProductSearchResultFacetSchema.parse({
|
|
84
|
+
identifier: facetIdentifier,
|
|
85
|
+
name: facetIdentifier.key,
|
|
86
|
+
values: []
|
|
87
|
+
});
|
|
88
|
+
for (const valueId in facetValues) {
|
|
89
|
+
const count = facetValues[valueId];
|
|
90
|
+
const facetValueIdentifier = FacetValueIdentifierSchema.parse({
|
|
91
|
+
facet: facetIdentifier,
|
|
92
|
+
key: valueId
|
|
93
|
+
});
|
|
94
|
+
result.values.push(this.parseFacetValue(facetValueIdentifier, valueId, count));
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
parseFacetValue(facetValueIdentifier, label, count) {
|
|
99
|
+
return ProductSearchResultFacetValueSchema.parse({
|
|
100
|
+
identifier: facetValueIdentifier,
|
|
101
|
+
name: label,
|
|
102
|
+
count,
|
|
103
|
+
active: false
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
parseInput(data) {
|
|
107
|
+
if (!this.isSearchResponse(data)) {
|
|
108
|
+
throw new Error("Invalid Algolia search response");
|
|
109
|
+
}
|
|
110
|
+
return data;
|
|
111
|
+
}
|
|
112
|
+
isSearchResponse(data) {
|
|
113
|
+
if (!data || typeof data !== "object") {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
if (!("hits" in data) || !Array.isArray(data.hits)) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
export {
|
|
123
|
+
AlgoliaProductSearchFactory
|
|
124
|
+
};
|
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@reactionary/algolia",
|
|
3
|
+
"version": "0.6.3",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./index.js",
|
|
6
|
+
"types": "./src/index.d.ts",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@reactionary/core": "0.6.3",
|
|
9
|
+
"zod": "4.1.9",
|
|
10
|
+
"algoliasearch": "^5.48.0",
|
|
11
|
+
"vitest": "^4.0.9",
|
|
12
|
+
"@nx/vite": "22.4.5"
|
|
13
|
+
},
|
|
14
|
+
"sideEffects": false
|
|
15
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { CapabilitiesSchema } from "@reactionary/core";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
const ProductSearchCapabilitySchema = z.looseObject({
|
|
4
|
+
enabled: z.boolean(),
|
|
5
|
+
factory: z.unknown().optional(),
|
|
6
|
+
capability: z.unknown().optional()
|
|
7
|
+
});
|
|
8
|
+
const CapabilityOverrideSchema = z.looseObject({
|
|
9
|
+
enabled: z.boolean(),
|
|
10
|
+
capability: z.unknown().optional()
|
|
11
|
+
});
|
|
12
|
+
const AlgoliaCapabilitiesSchema = CapabilitiesSchema.pick({
|
|
13
|
+
productSearch: true,
|
|
14
|
+
analytics: true,
|
|
15
|
+
productRecommendations: true
|
|
16
|
+
}).extend({
|
|
17
|
+
productSearch: ProductSearchCapabilitySchema.optional(),
|
|
18
|
+
analytics: CapabilityOverrideSchema.optional(),
|
|
19
|
+
productRecommendations: CapabilityOverrideSchema.optional()
|
|
20
|
+
}).partial();
|
|
21
|
+
export {
|
|
22
|
+
AlgoliaCapabilitiesSchema
|
|
23
|
+
};
|
package/schema/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ProductRecommendationIdentifierSchema } from "@reactionary/core";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
const AlgoliaProductSearchIdentifierSchema = ProductRecommendationIdentifierSchema.extend({
|
|
4
|
+
abTestID: z.number().optional(),
|
|
5
|
+
abTestVariantID: z.number().optional()
|
|
6
|
+
});
|
|
7
|
+
export {
|
|
8
|
+
AlgoliaProductSearchIdentifierSchema
|
|
9
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ProductSearchIdentifierSchema, ProductSearchResultSchema } from "@reactionary/core";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
const AlgoliaProductSearchIdentifierSchema = ProductSearchIdentifierSchema.extend({
|
|
4
|
+
key: z.string(),
|
|
5
|
+
index: z.string()
|
|
6
|
+
});
|
|
7
|
+
const AlgoliaProductSearchResultSchema = ProductSearchResultSchema.extend({
|
|
8
|
+
identifier: AlgoliaProductSearchIdentifierSchema.default(() => AlgoliaProductSearchIdentifierSchema.parse({}))
|
|
9
|
+
});
|
|
10
|
+
export {
|
|
11
|
+
AlgoliaProductSearchIdentifierSchema,
|
|
12
|
+
AlgoliaProductSearchResultSchema
|
|
13
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { AnalyticsCapability, type AnalyticsMutationProductAddToCartEvent, type AnalyticsMutationProductSummaryClickEvent, type AnalyticsMutationProductSummaryViewEvent, type AnalyticsMutationPurchaseEvent, type Cache, type RequestContext } from '@reactionary/core';
|
|
2
|
+
import { type InsightsClient } from 'algoliasearch';
|
|
3
|
+
import type { AlgoliaConfiguration } from '../schema/configuration.schema.js';
|
|
4
|
+
export declare class AlgoliaAnalyticsCapability extends AnalyticsCapability {
|
|
5
|
+
protected client: InsightsClient;
|
|
6
|
+
protected config: AlgoliaConfiguration;
|
|
7
|
+
constructor(cache: Cache, requestContext: RequestContext, config: AlgoliaConfiguration);
|
|
8
|
+
protected processProductAddToCart(event: AnalyticsMutationProductAddToCartEvent): Promise<void>;
|
|
9
|
+
protected processProductSummaryClick(event: AnalyticsMutationProductSummaryClickEvent): Promise<void>;
|
|
10
|
+
protected processProductSummaryView(event: AnalyticsMutationProductSummaryViewEvent): Promise<void>;
|
|
11
|
+
protected processPurchase(event: AnalyticsMutationPurchaseEvent): Promise<void>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { type Cache, ProductRecommendationsCapability, type ProductRecommendation, type ProductRecommendationAlgorithmFrequentlyBoughtTogetherQuery, type ProductRecommendationAlgorithmSimilarProductsQuery, type ProductRecommendationAlgorithmRelatedProductsQuery, type ProductRecommendationAlgorithmTrendingInCategoryQuery, type RequestContext, type ProductRecommendationsQuery, type ProductSearchResultItemVariant } from '@reactionary/core';
|
|
2
|
+
import { type RecommendationsResults, type RecommendSearchParams, type LiteClient } from 'algoliasearch/lite';
|
|
3
|
+
import type { AlgoliaConfiguration } from '../schema/configuration.schema.js';
|
|
4
|
+
import type { AlgoliaProductRecommendationIdentifier } from '../schema/product-recommendation.schema.js';
|
|
5
|
+
import type { AlgoliaNativeRecord, AlgoliaNativeVariant } from '../schema/search.schema.js';
|
|
6
|
+
/**
|
|
7
|
+
* AlgoliaProductRecommendationsCapability
|
|
8
|
+
*
|
|
9
|
+
* Provides product recommendations using Algolia's Recommend API.
|
|
10
|
+
* Supports frequentlyBoughtTogether, similar, related, and trendingInCategory algorithms.
|
|
11
|
+
*
|
|
12
|
+
* Note: This requires Algolia Recommend to be enabled and AI models to be trained.
|
|
13
|
+
* See: https://www.algolia.com/doc/guides/algolia-recommend/overview/
|
|
14
|
+
*/
|
|
15
|
+
export declare class AlgoliaProductRecommendationsCapability extends ProductRecommendationsCapability {
|
|
16
|
+
protected config: AlgoliaConfiguration;
|
|
17
|
+
protected client: LiteClient;
|
|
18
|
+
constructor(config: AlgoliaConfiguration, cache: Cache, context: RequestContext);
|
|
19
|
+
protected getRecommendationThreshold(_algorithm: string): number;
|
|
20
|
+
protected getQueryParametersForRecommendations(algorithm: string): RecommendSearchParams;
|
|
21
|
+
/**
|
|
22
|
+
* Get frequently bought together recommendations using Algolia Recommend
|
|
23
|
+
*/
|
|
24
|
+
protected getFrequentlyBoughtTogetherRecommendations(query: ProductRecommendationAlgorithmFrequentlyBoughtTogetherQuery): Promise<ProductRecommendation[]>;
|
|
25
|
+
/**
|
|
26
|
+
* Get similar product recommendations using Algolia Recommend
|
|
27
|
+
*/
|
|
28
|
+
protected getSimilarProductsRecommendations(query: ProductRecommendationAlgorithmSimilarProductsQuery): Promise<ProductRecommendation[]>;
|
|
29
|
+
/**
|
|
30
|
+
* Get related product recommendations using Algolia Recommend
|
|
31
|
+
*/
|
|
32
|
+
protected getRelatedProductsRecommendations(query: ProductRecommendationAlgorithmRelatedProductsQuery): Promise<ProductRecommendation[]>;
|
|
33
|
+
/**
|
|
34
|
+
* Get trending in category recommendations using Algolia Recommend
|
|
35
|
+
*/
|
|
36
|
+
protected getTrendingInCategoryRecommendations(query: ProductRecommendationAlgorithmTrendingInCategoryQuery): Promise<ProductRecommendation[]>;
|
|
37
|
+
protected parseRecommendation(res: RecommendationsResults, query: ProductRecommendationsQuery): (({
|
|
38
|
+
recommendationIdentifier: {
|
|
39
|
+
key: string;
|
|
40
|
+
algorithm: string;
|
|
41
|
+
};
|
|
42
|
+
recommendationReturnType: "idOnly";
|
|
43
|
+
product: {
|
|
44
|
+
key: string;
|
|
45
|
+
};
|
|
46
|
+
} & {
|
|
47
|
+
_?: never;
|
|
48
|
+
}) | ({
|
|
49
|
+
recommendationIdentifier: {
|
|
50
|
+
key: string;
|
|
51
|
+
algorithm: string;
|
|
52
|
+
};
|
|
53
|
+
recommendationReturnType: "productSearchResultItem";
|
|
54
|
+
product: {
|
|
55
|
+
identifier: {
|
|
56
|
+
key: string;
|
|
57
|
+
};
|
|
58
|
+
name: string;
|
|
59
|
+
slug: string;
|
|
60
|
+
variants: {
|
|
61
|
+
variant: {
|
|
62
|
+
sku: string;
|
|
63
|
+
};
|
|
64
|
+
image: {
|
|
65
|
+
sourceUrl: string;
|
|
66
|
+
altText: string;
|
|
67
|
+
width?: number | undefined;
|
|
68
|
+
height?: number | undefined;
|
|
69
|
+
};
|
|
70
|
+
options?: {
|
|
71
|
+
identifier: {
|
|
72
|
+
key: string;
|
|
73
|
+
};
|
|
74
|
+
name: string;
|
|
75
|
+
value: {
|
|
76
|
+
identifier: {
|
|
77
|
+
option: {
|
|
78
|
+
key: string;
|
|
79
|
+
};
|
|
80
|
+
key: string;
|
|
81
|
+
};
|
|
82
|
+
label: string;
|
|
83
|
+
};
|
|
84
|
+
} | undefined;
|
|
85
|
+
}[];
|
|
86
|
+
};
|
|
87
|
+
} & {
|
|
88
|
+
_?: never;
|
|
89
|
+
}))[];
|
|
90
|
+
/**
|
|
91
|
+
* Maps Algolia recommendation results to ProductRecommendation format
|
|
92
|
+
*/
|
|
93
|
+
protected parseSingle(hit: AlgoliaNativeRecord, recommendationIdentifier: AlgoliaProductRecommendationIdentifier): ProductRecommendation;
|
|
94
|
+
protected parseSearchResultItem(body: AlgoliaNativeRecord): {
|
|
95
|
+
identifier: {
|
|
96
|
+
key: string;
|
|
97
|
+
};
|
|
98
|
+
name: string;
|
|
99
|
+
slug: string;
|
|
100
|
+
variants: ProductSearchResultItemVariant[];
|
|
101
|
+
};
|
|
102
|
+
protected parseVariant(variant: AlgoliaNativeVariant, product: AlgoliaNativeRecord): ProductSearchResultItemVariant;
|
|
103
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Cache, type FacetValueIdentifier, ProductSearchCapability, type ProductSearchFactory, type ProductSearchFactoryOutput, type ProductSearchFactoryWithOutput, type ProductSearchQueryByTerm, type ProductSearchQueryCreateNavigationFilter, type RequestContext, type Result } from '@reactionary/core';
|
|
2
|
+
import type { AlgoliaConfiguration } from '../schema/configuration.schema.js';
|
|
3
|
+
import type { AlgoliaProductSearchFactory } from '../factories/product-search/product-search.factory.js';
|
|
4
|
+
export declare class AlgoliaProductSearchCapability<TFactory extends ProductSearchFactory = AlgoliaProductSearchFactory> extends ProductSearchCapability<ProductSearchFactoryOutput<TFactory>> {
|
|
5
|
+
protected config: AlgoliaConfiguration;
|
|
6
|
+
protected factory: ProductSearchFactoryWithOutput<TFactory>;
|
|
7
|
+
constructor(cache: Cache, context: RequestContext, config: AlgoliaConfiguration, factory: ProductSearchFactoryWithOutput<TFactory>);
|
|
8
|
+
protected queryByTermPayload(payload: ProductSearchQueryByTerm): {
|
|
9
|
+
indexName: string;
|
|
10
|
+
query: string;
|
|
11
|
+
page: number;
|
|
12
|
+
hitsPerPage: number;
|
|
13
|
+
facets: string[];
|
|
14
|
+
analytics: boolean;
|
|
15
|
+
clickAnalytics: boolean;
|
|
16
|
+
facetFilters: string[];
|
|
17
|
+
filters: string;
|
|
18
|
+
};
|
|
19
|
+
queryByTerm(payload: ProductSearchQueryByTerm): Promise<Result<ProductSearchFactoryOutput<TFactory>>>;
|
|
20
|
+
createCategoryNavigationFilter(payload: ProductSearchQueryCreateNavigationFilter): Promise<Result<FacetValueIdentifier>>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Cache, RequestContext } from '@reactionary/core';
|
|
2
|
+
import type { AlgoliaCapabilities } from '../schema/capabilities.schema.js';
|
|
3
|
+
import type { AlgoliaConfiguration } from '../schema/configuration.schema.js';
|
|
4
|
+
import { type AlgoliaClientFromCapabilities } from './initialize.types.js';
|
|
5
|
+
export declare function withAlgoliaCapabilities<T extends AlgoliaCapabilities>(configuration: AlgoliaConfiguration, capabilities: T): (cache: Cache, context: RequestContext) => AlgoliaClientFromCapabilities<T>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ClientFromCapabilities, ProductSearchFactory } from '@reactionary/core';
|
|
2
|
+
import type { AlgoliaCapabilities } from '../schema/capabilities.schema.js';
|
|
3
|
+
import type { AlgoliaProductSearchFactory } from '../factories/product-search/product-search.factory.js';
|
|
4
|
+
import type { AlgoliaAnalyticsCapability } from '../capabilities/analytics.capability.js';
|
|
5
|
+
import type { AlgoliaProductRecommendationsCapability } from '../capabilities/product-recommendations.capability.js';
|
|
6
|
+
import type { AlgoliaProductSearchCapability } from '../capabilities/product-search.capability.js';
|
|
7
|
+
type EnabledCapability<TCapability> = TCapability extends {
|
|
8
|
+
enabled: true;
|
|
9
|
+
} ? true : false;
|
|
10
|
+
type NormalizeConfiguredCapabilities<T extends AlgoliaCapabilities> = Omit<T, 'productSearch' | 'analytics' | 'productRecommendations'> & {
|
|
11
|
+
productSearch?: EnabledCapability<T['productSearch']>;
|
|
12
|
+
analytics?: EnabledCapability<T['analytics']>;
|
|
13
|
+
productRecommendations?: EnabledCapability<T['productRecommendations']>;
|
|
14
|
+
};
|
|
15
|
+
type ExtractCapabilityFactory<TCapability, TContract, TDefaultFactory> = TCapability extends {
|
|
16
|
+
enabled: true;
|
|
17
|
+
factory?: infer TFactory;
|
|
18
|
+
} ? TFactory extends TContract ? TFactory : TDefaultFactory : TDefaultFactory;
|
|
19
|
+
type ExtractCapabilityImplementation<TCapability, TDefaultCapability> = TCapability extends {
|
|
20
|
+
enabled: true;
|
|
21
|
+
capability?: infer TCapabilityFactory;
|
|
22
|
+
} ? TCapabilityFactory extends (...args: unknown[]) => infer TResolvedCapability ? TResolvedCapability : TDefaultCapability : TDefaultCapability;
|
|
23
|
+
type CapabilityOverride<TCapability, TKey extends string, TResolvedCapability> = TCapability extends {
|
|
24
|
+
enabled: true;
|
|
25
|
+
} ? {
|
|
26
|
+
[K in TKey]: TResolvedCapability;
|
|
27
|
+
} : Record<never, never>;
|
|
28
|
+
type ProductSearchFactoryFor<T extends AlgoliaCapabilities> = ExtractCapabilityFactory<T['productSearch'], ProductSearchFactory, AlgoliaProductSearchFactory>;
|
|
29
|
+
type ProductSearchCapabilityFor<T extends AlgoliaCapabilities> = ExtractCapabilityImplementation<T['productSearch'], AlgoliaProductSearchCapability<ProductSearchFactoryFor<T>>>;
|
|
30
|
+
type AnalyticsCapabilityFor<T extends AlgoliaCapabilities> = ExtractCapabilityImplementation<T['analytics'], AlgoliaAnalyticsCapability>;
|
|
31
|
+
type ProductRecommendationsCapabilityFor<T extends AlgoliaCapabilities> = ExtractCapabilityImplementation<T['productRecommendations'], AlgoliaProductRecommendationsCapability>;
|
|
32
|
+
export type AlgoliaClientFromCapabilities<T extends AlgoliaCapabilities> = Omit<ClientFromCapabilities<NormalizeConfiguredCapabilities<T>>, 'productSearch' | 'analytics' | 'productRecommendations'> & CapabilityOverride<T['productSearch'], 'productSearch', ProductSearchCapabilityFor<T>> & CapabilityOverride<T['analytics'], 'analytics', AnalyticsCapabilityFor<T>> & CapabilityOverride<T['productRecommendations'], 'productRecommendations', ProductRecommendationsCapabilityFor<T>>;
|
|
33
|
+
export declare function resolveCapabilityWithFactory<TFactory, TResolvedCapability, TCapabilityArgs>(capability: {
|
|
34
|
+
factory?: TFactory;
|
|
35
|
+
capability?: (args: TCapabilityArgs) => TResolvedCapability;
|
|
36
|
+
} | undefined, defaults: {
|
|
37
|
+
factory: TFactory;
|
|
38
|
+
capability: (args: TCapabilityArgs) => TResolvedCapability;
|
|
39
|
+
}, buildCapabilityArgs: (factory: TFactory) => TCapabilityArgs): TResolvedCapability;
|
|
40
|
+
export declare function resolveDirectCapability<TResolvedCapability, TCapabilityArgs>(capability: {
|
|
41
|
+
capability?: (args: TCapabilityArgs) => TResolvedCapability;
|
|
42
|
+
} | undefined, defaultCapability: (args: TCapabilityArgs) => TResolvedCapability, args: TCapabilityArgs): TResolvedCapability;
|
|
43
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './product-search/product-search.factory.js';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AnyProductSearchResultSchema, FacetIdentifier, FacetValueIdentifier, ProductSearchFactory, ProductSearchQueryByTerm, ProductSearchResultFacet, ProductSearchResultFacetValue, ProductSearchResultItem, ProductSearchResultItemVariant, RequestContext } from '@reactionary/core';
|
|
2
|
+
import type * as z from 'zod';
|
|
3
|
+
import type { SearchResponse } from 'algoliasearch';
|
|
4
|
+
import type { AlgoliaNativeRecord, AlgoliaNativeVariant } from '../../schema/search.schema.js';
|
|
5
|
+
import type { AlgoliaProductSearchResultSchema } from '../../schema/search.schema.js';
|
|
6
|
+
export declare class AlgoliaProductSearchFactory<TProductSearchResultSchema extends AnyProductSearchResultSchema = typeof AlgoliaProductSearchResultSchema> implements ProductSearchFactory<TProductSearchResultSchema> {
|
|
7
|
+
readonly productSearchResultSchema: TProductSearchResultSchema;
|
|
8
|
+
constructor(productSearchResultSchema: TProductSearchResultSchema);
|
|
9
|
+
parseSearchResult(_context: RequestContext, data: unknown, query: ProductSearchQueryByTerm): z.output<TProductSearchResultSchema>;
|
|
10
|
+
protected parseSingle(body: AlgoliaNativeRecord): ProductSearchResultItem;
|
|
11
|
+
protected parseVariant(variant: AlgoliaNativeVariant, product: AlgoliaNativeRecord): ProductSearchResultItemVariant;
|
|
12
|
+
protected parseFacet(facetIdentifier: FacetIdentifier, facetValues: Record<string, number>): ProductSearchResultFacet;
|
|
13
|
+
protected parseFacetValue(facetValueIdentifier: FacetValueIdentifier, label: string, count: number): ProductSearchResultFacetValue;
|
|
14
|
+
protected parseInput(data: unknown): SearchResponse<AlgoliaNativeRecord>;
|
|
15
|
+
protected isSearchResponse(data: unknown): data is SearchResponse<AlgoliaNativeRecord>;
|
|
16
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { AnalyticsCapability, Cache, ProductRecommendationsCapability, ProductSearchFactory, ProductSearchFactoryWithOutput, ProductSearchCapability, RequestContext } from '@reactionary/core';
|
|
2
|
+
import type { AlgoliaConfiguration } from './configuration.schema.js';
|
|
3
|
+
import * as z from 'zod';
|
|
4
|
+
export declare const AlgoliaCapabilitiesSchema: z.ZodObject<{
|
|
5
|
+
productSearch: z.ZodOptional<z.ZodOptional<z.ZodObject<{
|
|
6
|
+
enabled: z.ZodBoolean;
|
|
7
|
+
factory: z.ZodOptional<z.ZodUnknown>;
|
|
8
|
+
capability: z.ZodOptional<z.ZodUnknown>;
|
|
9
|
+
}, z.core.$loose>>>;
|
|
10
|
+
analytics: z.ZodOptional<z.ZodOptional<z.ZodObject<{
|
|
11
|
+
enabled: z.ZodBoolean;
|
|
12
|
+
capability: z.ZodOptional<z.ZodUnknown>;
|
|
13
|
+
}, z.core.$loose>>>;
|
|
14
|
+
productRecommendations: z.ZodOptional<z.ZodOptional<z.ZodObject<{
|
|
15
|
+
enabled: z.ZodBoolean;
|
|
16
|
+
capability: z.ZodOptional<z.ZodUnknown>;
|
|
17
|
+
}, z.core.$loose>>>;
|
|
18
|
+
}, z.core.$loose>;
|
|
19
|
+
export interface AlgoliaCapabilityFactoryArgs {
|
|
20
|
+
cache: Cache;
|
|
21
|
+
context: RequestContext;
|
|
22
|
+
config: AlgoliaConfiguration;
|
|
23
|
+
}
|
|
24
|
+
export interface AlgoliaProductSearchCapabilityFactoryArgs<TFactory extends ProductSearchFactory = ProductSearchFactory> extends AlgoliaCapabilityFactoryArgs {
|
|
25
|
+
factory: ProductSearchFactoryWithOutput<TFactory>;
|
|
26
|
+
}
|
|
27
|
+
export interface AlgoliaProductSearchCapabilityConfig<TFactory extends ProductSearchFactory = ProductSearchFactory, TCapability extends ProductSearchCapability = ProductSearchCapability> {
|
|
28
|
+
enabled: boolean;
|
|
29
|
+
factory?: ProductSearchFactoryWithOutput<TFactory>;
|
|
30
|
+
capability?: (args: AlgoliaProductSearchCapabilityFactoryArgs<TFactory>) => TCapability;
|
|
31
|
+
}
|
|
32
|
+
export interface AlgoliaAnalyticsCapabilityConfig<TCapability extends AnalyticsCapability = AnalyticsCapability> {
|
|
33
|
+
enabled: boolean;
|
|
34
|
+
capability?: (args: AlgoliaCapabilityFactoryArgs) => TCapability;
|
|
35
|
+
}
|
|
36
|
+
export interface AlgoliaProductRecommendationsCapabilityConfig<TCapability extends ProductRecommendationsCapability = ProductRecommendationsCapability> {
|
|
37
|
+
enabled: boolean;
|
|
38
|
+
capability?: (args: AlgoliaCapabilityFactoryArgs) => TCapability;
|
|
39
|
+
}
|
|
40
|
+
export type AlgoliaCapabilities<TProductSearchFactory extends ProductSearchFactory = ProductSearchFactory, TProductSearchCapability extends ProductSearchCapability = ProductSearchCapability, TAnalyticsCapability extends AnalyticsCapability = AnalyticsCapability, TProductRecommendationsCapability extends ProductRecommendationsCapability = ProductRecommendationsCapability> = {
|
|
41
|
+
productSearch?: AlgoliaProductSearchCapabilityConfig<TProductSearchFactory, TProductSearchCapability>;
|
|
42
|
+
analytics?: AlgoliaAnalyticsCapabilityConfig<TAnalyticsCapability>;
|
|
43
|
+
productRecommendations?: AlgoliaProductRecommendationsCapabilityConfig<TProductRecommendationsCapability>;
|
|
44
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
export declare const AlgoliaProductSearchIdentifierSchema: z.ZodObject<{
|
|
3
|
+
key: z.ZodString;
|
|
4
|
+
algorithm: z.ZodString;
|
|
5
|
+
abTestID: z.ZodOptional<z.ZodNumber>;
|
|
6
|
+
abTestVariantID: z.ZodOptional<z.ZodNumber>;
|
|
7
|
+
}, z.core.$loose>;
|
|
8
|
+
export type AlgoliaProductRecommendationIdentifier = z.infer<typeof AlgoliaProductSearchIdentifierSchema>;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as z from 'zod';
|
|
2
|
+
export declare const AlgoliaProductSearchIdentifierSchema: z.ZodObject<{
|
|
3
|
+
term: z.ZodString;
|
|
4
|
+
facets: z.ZodArray<z.ZodObject<{
|
|
5
|
+
facet: z.ZodObject<{
|
|
6
|
+
key: z.ZodString;
|
|
7
|
+
}, z.core.$loose>;
|
|
8
|
+
key: z.ZodString;
|
|
9
|
+
}, z.core.$strip>>;
|
|
10
|
+
filters: z.ZodArray<z.ZodString>;
|
|
11
|
+
paginationOptions: z.ZodObject<{
|
|
12
|
+
pageNumber: z.ZodDefault<z.ZodNumber>;
|
|
13
|
+
pageSize: z.ZodDefault<z.ZodNumber>;
|
|
14
|
+
}, z.core.$loose>;
|
|
15
|
+
categoryFilter: z.ZodOptional<z.ZodObject<{
|
|
16
|
+
facet: z.ZodObject<{
|
|
17
|
+
key: z.ZodString;
|
|
18
|
+
}, z.core.$loose>;
|
|
19
|
+
key: z.ZodString;
|
|
20
|
+
}, z.core.$strip>>;
|
|
21
|
+
key: z.ZodString;
|
|
22
|
+
index: z.ZodString;
|
|
23
|
+
}, z.core.$loose>;
|
|
24
|
+
export declare const AlgoliaProductSearchResultSchema: z.ZodObject<{
|
|
25
|
+
pageNumber: z.ZodNumber;
|
|
26
|
+
pageSize: z.ZodNumber;
|
|
27
|
+
totalCount: z.ZodNumber;
|
|
28
|
+
totalPages: z.ZodNumber;
|
|
29
|
+
items: z.ZodArray<z.ZodObject<{
|
|
30
|
+
identifier: z.ZodObject<{
|
|
31
|
+
key: z.ZodString;
|
|
32
|
+
}, z.core.$loose>;
|
|
33
|
+
name: z.ZodString;
|
|
34
|
+
slug: z.ZodString;
|
|
35
|
+
variants: z.ZodArray<z.ZodObject<{
|
|
36
|
+
variant: z.ZodObject<{
|
|
37
|
+
sku: z.ZodString;
|
|
38
|
+
}, z.core.$loose>;
|
|
39
|
+
image: z.ZodObject<{
|
|
40
|
+
sourceUrl: z.ZodDefault<z.ZodString>;
|
|
41
|
+
altText: z.ZodDefault<z.ZodString>;
|
|
42
|
+
width: z.ZodOptional<z.ZodNumber>;
|
|
43
|
+
height: z.ZodOptional<z.ZodNumber>;
|
|
44
|
+
}, z.core.$loose>;
|
|
45
|
+
options: z.ZodOptional<z.ZodObject<{
|
|
46
|
+
identifier: z.ZodObject<{
|
|
47
|
+
key: z.ZodString;
|
|
48
|
+
}, z.core.$loose>;
|
|
49
|
+
name: z.ZodString;
|
|
50
|
+
value: z.ZodObject<{
|
|
51
|
+
identifier: z.ZodObject<{
|
|
52
|
+
option: z.ZodObject<{
|
|
53
|
+
key: z.ZodString;
|
|
54
|
+
}, z.core.$loose>;
|
|
55
|
+
key: z.ZodString;
|
|
56
|
+
}, z.core.$loose>;
|
|
57
|
+
label: z.ZodString;
|
|
58
|
+
}, z.core.$loose>;
|
|
59
|
+
}, z.core.$loose>>;
|
|
60
|
+
}, z.core.$loose>>;
|
|
61
|
+
}, z.core.$loose>>;
|
|
62
|
+
facets: z.ZodArray<z.ZodObject<{
|
|
63
|
+
identifier: z.ZodObject<{
|
|
64
|
+
key: z.ZodString;
|
|
65
|
+
}, z.core.$loose>;
|
|
66
|
+
name: z.ZodString;
|
|
67
|
+
values: z.ZodArray<z.ZodObject<{
|
|
68
|
+
identifier: z.ZodObject<{
|
|
69
|
+
facet: z.ZodObject<{
|
|
70
|
+
key: z.ZodString;
|
|
71
|
+
}, z.core.$loose>;
|
|
72
|
+
key: z.ZodString;
|
|
73
|
+
}, z.core.$strip>;
|
|
74
|
+
name: z.ZodString;
|
|
75
|
+
count: z.ZodNumber;
|
|
76
|
+
active: z.ZodBoolean;
|
|
77
|
+
}, z.core.$loose>>;
|
|
78
|
+
}, z.core.$loose>>;
|
|
79
|
+
identifier: z.ZodDefault<z.ZodObject<{
|
|
80
|
+
term: z.ZodString;
|
|
81
|
+
facets: z.ZodArray<z.ZodObject<{
|
|
82
|
+
facet: z.ZodObject<{
|
|
83
|
+
key: z.ZodString;
|
|
84
|
+
}, z.core.$loose>;
|
|
85
|
+
key: z.ZodString;
|
|
86
|
+
}, z.core.$strip>>;
|
|
87
|
+
filters: z.ZodArray<z.ZodString>;
|
|
88
|
+
paginationOptions: z.ZodObject<{
|
|
89
|
+
pageNumber: z.ZodDefault<z.ZodNumber>;
|
|
90
|
+
pageSize: z.ZodDefault<z.ZodNumber>;
|
|
91
|
+
}, z.core.$loose>;
|
|
92
|
+
categoryFilter: z.ZodOptional<z.ZodObject<{
|
|
93
|
+
facet: z.ZodObject<{
|
|
94
|
+
key: z.ZodString;
|
|
95
|
+
}, z.core.$loose>;
|
|
96
|
+
key: z.ZodString;
|
|
97
|
+
}, z.core.$strip>>;
|
|
98
|
+
key: z.ZodString;
|
|
99
|
+
index: z.ZodString;
|
|
100
|
+
}, z.core.$loose>>;
|
|
101
|
+
}, z.core.$strip>;
|
|
102
|
+
export type AlgoliaProductSearchResult = z.infer<typeof AlgoliaProductSearchResultSchema>;
|
|
103
|
+
export type AlgoliaProductSearchIdentifier = z.infer<typeof AlgoliaProductSearchIdentifierSchema>;
|
|
104
|
+
export interface AlgoliaNativeVariant {
|
|
105
|
+
sku: string;
|
|
106
|
+
image: string;
|
|
107
|
+
}
|
|
108
|
+
export interface AlgoliaNativeRecord {
|
|
109
|
+
objectID: string;
|
|
110
|
+
slug?: string;
|
|
111
|
+
name?: string;
|
|
112
|
+
variants: Array<AlgoliaNativeVariant>;
|
|
113
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ClientBuilder,
|
|
3
|
+
NoOpCache,
|
|
4
|
+
createInitialRequestContext
|
|
5
|
+
} from "@reactionary/core";
|
|
6
|
+
import * as z from "zod";
|
|
7
|
+
import { withAlgoliaCapabilities } from "../core/initialize.js";
|
|
8
|
+
import { AlgoliaProductSearchFactory } from "../factories/product-search/product-search.factory.js";
|
|
9
|
+
import { AlgoliaProductSearchResultSchema } from "../schema/search.schema.js";
|
|
10
|
+
const assertType = (_value) => {
|
|
11
|
+
};
|
|
12
|
+
const assertNotAny = (_value) => {
|
|
13
|
+
};
|
|
14
|
+
const ExtendedProductSearchResultSchema = AlgoliaProductSearchResultSchema.safeExtend(
|
|
15
|
+
{
|
|
16
|
+
extendedMeta: z.string()
|
|
17
|
+
}
|
|
18
|
+
);
|
|
19
|
+
class ExtendedAlgoliaProductSearchFactory extends AlgoliaProductSearchFactory {
|
|
20
|
+
constructor() {
|
|
21
|
+
super(ExtendedProductSearchResultSchema);
|
|
22
|
+
}
|
|
23
|
+
parseSearchResult(context, data, query) {
|
|
24
|
+
const base = super.parseSearchResult(context, data, query);
|
|
25
|
+
return this.productSearchResultSchema.parse({
|
|
26
|
+
...base,
|
|
27
|
+
extendedMeta: "from-factory"
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const config = {
|
|
32
|
+
appId: "ALGOLIA_APP_ID",
|
|
33
|
+
apiKey: "ALGOLIA_API_KEY",
|
|
34
|
+
indexName: "ALGOLIA_INDEX"
|
|
35
|
+
};
|
|
36
|
+
const client = new ClientBuilder(createInitialRequestContext()).withCache(new NoOpCache()).withCapability(
|
|
37
|
+
withAlgoliaCapabilities(config, {
|
|
38
|
+
productSearch: {
|
|
39
|
+
enabled: true,
|
|
40
|
+
factory: new ExtendedAlgoliaProductSearchFactory()
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
).build();
|
|
44
|
+
client.productSearch.queryByTerm({
|
|
45
|
+
search: {
|
|
46
|
+
term: "test",
|
|
47
|
+
facets: [],
|
|
48
|
+
filters: [],
|
|
49
|
+
paginationOptions: {
|
|
50
|
+
pageNumber: 1,
|
|
51
|
+
pageSize: 10
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}).then((result) => {
|
|
55
|
+
assertNotAny(result);
|
|
56
|
+
if (result.success) {
|
|
57
|
+
assertNotAny(result.value);
|
|
58
|
+
assertNotAny(result.value.extendedMeta);
|
|
59
|
+
assertType(result.value.extendedMeta);
|
|
60
|
+
}
|
|
61
|
+
});
|