@reactionary/source 0.3.5 → 0.3.7

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.
@@ -1,10 +1,25 @@
1
1
  import z from "zod";
2
2
  import { ProductIdentifierSchema, ProductRecommendationIdentifierSchema, ProductVariantIdentifierSchema } from "./identifiers.model.js";
3
+ import { ProductSearchResultItemSchema } from "./product-search.model.js";
3
4
 
4
- export const ProductRecommendationSchema = z.looseObject({
5
+ export const BaseProductRecommendationSchema = z.looseObject({
5
6
  recommendationIdentifier: ProductRecommendationIdentifierSchema.describe('The identifier for the product recommendation, which includes a key and an algorithm and any other vendor specific/instance specific data '),
7
+ });
8
+
9
+ export const ProductRecommendationIdOnlySchema = BaseProductRecommendationSchema.extend({
10
+ recommendationReturnType: z.literal('idOnly').describe('The type of recommendation return'),
6
11
  product: ProductIdentifierSchema.describe('The identifier for the recommended product.'),
7
12
  });
13
+ export const ProductRecommendationProductSearchResultItemSchema = BaseProductRecommendationSchema.extend({
14
+ recommendationReturnType: z.literal('productSearchResultItem').describe('The type of recommendation return'),
15
+ product: ProductSearchResultItemSchema.describe('The recommended product, including its identifier, name, slug, and variants. This can be used to display the recommended product directly on the frontend without needing to make an additional request to fetch the product details.'),
16
+ });
8
17
 
18
+ export const ProductRecommendationSchema = z.discriminatedUnion('recommendationReturnType', [
19
+ ProductRecommendationIdOnlySchema,
20
+ ProductRecommendationProductSearchResultItemSchema
21
+ ]);
9
22
 
23
+ export type ProductRecommendationIdOnly = z.infer<typeof ProductRecommendationIdOnlySchema>;
24
+ export type ProductRecommendationSearchItem = z.infer<typeof ProductRecommendationProductSearchResultItemSchema>;
10
25
  export type ProductRecommendation = z.infer<typeof ProductRecommendationSchema>;
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { IdentityIdentifierSchema, WebStoreIdentifierSchema } from './models/identifiers.model.js';
2
+ import { WebStoreIdentifierSchema } from './models/identifiers.model.js';
3
3
  import { CurrencySchema } from './models/currency.model.js';
4
4
  import { IdentitySchema } from './models/identity.model.js';
5
5
 
@@ -14,12 +14,12 @@ export const LanguageContextSchema = z.looseObject( {
14
14
  export const IdentityContextSchema = z.looseObject({
15
15
  identity: IdentitySchema,
16
16
  personalizationKey: z.string(),
17
- lastUpdated: z.date()
17
+ lastUpdated: z.coerce.date()
18
18
  });
19
19
 
20
- export const SessionSchema = z.record(z.string(), z.any()).and(z.object({
20
+ export const SessionSchema = z.looseObject({
21
21
  identityContext: IdentityContextSchema
22
- }));
22
+ });
23
23
 
24
24
  export const TaxJurisdictionSchema = z.object( {
25
25
  countryCode: z.string().default('US'),
@@ -0,0 +1,15 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createInitialRequestContext } from "../initialization.js";
3
+ import { RequestContextSchema } from "../schemas/session.schema.js";
4
+
5
+ describe('Request Context', () => {
6
+ it('should be able to serialize the request context as a JSON string, and have it parse', async () => {
7
+ const context = createInitialRequestContext();
8
+ const contextString = JSON.stringify(context);
9
+ const reconstructedContext = JSON.parse(contextString);
10
+
11
+ const parse = RequestContextSchema.safeParse(reconstructedContext);
12
+
13
+ expect(parse.success).toBe(true);
14
+ });
15
+ });
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@reactionary/examples-node",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "main": "index.js",
5
5
  "types": "src/index.d.ts",
6
6
  "dependencies": {
7
- "@reactionary/core": "0.3.5",
8
- "@reactionary/provider-commercetools": "0.3.5",
9
- "@reactionary/provider-algolia": "0.3.5",
10
- "@reactionary/provider-medusa": "0.3.5",
11
- "@reactionary/provider-meilisearch": "0.3.5"
7
+ "@reactionary/core": "0.3.7",
8
+ "@reactionary/provider-commercetools": "0.3.7",
9
+ "@reactionary/provider-algolia": "0.3.7",
10
+ "@reactionary/provider-medusa": "0.3.7",
11
+ "@reactionary/provider-meilisearch": "0.3.7"
12
12
  },
13
13
  "type": "module"
14
14
  }
@@ -36,6 +36,17 @@ describe.each([PrimaryProvider.MEDUSA])(
36
36
  }
37
37
 
38
38
  expect(result.value.length).toBeGreaterThan(0);
39
+
40
+ expect(result.value[0].recommendationReturnType).toBe('productSearchResultItem');
41
+ if (result.value[0].recommendationReturnType === 'productSearchResultItem') {
42
+ expect(result.value[0].product.identifier.key).toBeDefined();
43
+ expect(result.value[0].product.name).toBeDefined();
44
+ expect(result.value[0].product.slug).toBeDefined();
45
+ expect(result.value[0].product.variants).toBeDefined();
46
+ expect(result.value[0].product.variants.length).toBeGreaterThan(0);
47
+ expect(result.value[0].product.variants[0].variant.sku).toBeDefined();
48
+ expect(result.value[0].product.variants[0].image.sourceUrl).toBeDefined();
49
+ }
39
50
  });
40
51
 
41
52
  it('should return an empty result for an unknown collection', async () => {
@@ -76,7 +87,16 @@ describe.each([PrimaryProvider.MEILISEARCH])(
76
87
  }
77
88
 
78
89
  expect(result.value.length).toBeGreaterThan(0);
79
- expect(result.value[0].product.key).toBeDefined();
90
+ expect(result.value[0].recommendationReturnType).toBe('productSearchResultItem');
91
+ if (result.value[0].recommendationReturnType === 'productSearchResultItem') {
92
+ expect(result.value[0].product.identifier.key).toBeDefined();
93
+ expect(result.value[0].product.name).toBeDefined();
94
+ expect(result.value[0].product.slug).toBeDefined();
95
+ expect(result.value[0].product.variants).toBeDefined();
96
+ expect(result.value[0].product.variants.length).toBeGreaterThan(0);
97
+ expect(result.value[0].product.variants[0].variant.sku).toBeDefined();
98
+ expect(result.value[0].product.variants[0].image.sourceUrl).toBeDefined();
99
+ }
80
100
  expect(result.value[0].recommendationIdentifier.key).toBeDefined();
81
101
  });
82
102
 
@@ -122,6 +142,17 @@ describe.each([PrimaryProvider.ALGOLIA])(
122
142
  }
123
143
 
124
144
  expect(result.value.length).toBeGreaterThan(0);
145
+ expect(result.value[0].recommendationReturnType).toBe('productSearchResultItem');
146
+ if (result.value[0].recommendationReturnType === 'productSearchResultItem') {
147
+ expect(result.value[0].product.identifier.key).toBeDefined();
148
+ expect(result.value[0].product.name).toBeDefined();
149
+ expect(result.value[0].product.slug).toBeDefined();
150
+ expect(result.value[0].product.variants).toBeDefined();
151
+ expect(result.value[0].product.variants.length).toBeGreaterThan(0);
152
+ expect(result.value[0].product.variants[0].variant.sku).toBeDefined();
153
+ expect(result.value[0].product.variants[0].image.sourceUrl).toBeDefined();
154
+ }
155
+
125
156
  });
126
157
 
127
158
  it('should return an empty result for an unknown sku', async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reactionary/source",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "dependencies": {
@@ -8,6 +8,10 @@ import {
8
8
  type ProductRecommendationAlgorithmTrendingInCategoryQuery,
9
9
  type RequestContext,
10
10
  type ProductRecommendationsQuery,
11
+ type ProductSearchResultItem,
12
+ type ProductSearchResultItemVariant,
13
+ ProductSearchResultItemVariantSchema,
14
+ ImageSchema,
11
15
  } from '@reactionary/core';
12
16
  import {
13
17
  liteClient,
@@ -21,12 +25,8 @@ import {
21
25
  } from 'algoliasearch/lite';
22
26
  import type { AlgoliaConfiguration } from '../schema/configuration.schema.js';
23
27
  import type { AlgoliaProductRecommendationIdentifier } from '../schema/product-recommendation.schema.js';
28
+ import type { AlgoliaNativeRecord, AlgoliaNativeVariant } from '../schema/search.schema.js';
24
29
 
25
- interface AlgoliaRecommendHit {
26
- objectID: string;
27
- sku?: string;
28
- [key: string]: unknown;
29
- }
30
30
 
31
31
  /**
32
32
  * AlgoliaProductRecommendationsProvider
@@ -209,7 +209,7 @@ export class AlgoliaProductRecommendationsProvider extends ProductRecommendation
209
209
 
210
210
  protected parseRecommendation(res: RecommendationsResults, query: ProductRecommendationsQuery) {
211
211
  const result = [];
212
- for(const hit of res.hits as AlgoliaRecommendHit[]) {
212
+ for(const hit of res.hits as AlgoliaNativeRecord[]) {
213
213
  const recommendationIdentifier = {
214
214
  key: res.queryID || 'x',
215
215
  algorithm: query.algorithm,
@@ -225,12 +225,40 @@ export class AlgoliaProductRecommendationsProvider extends ProductRecommendation
225
225
  /**
226
226
  * Maps Algolia recommendation results to ProductRecommendation format
227
227
  */
228
- protected parseSingle(hit: AlgoliaRecommendHit, recommendationIdentifier: AlgoliaProductRecommendationIdentifier): ProductRecommendation {
228
+ protected parseSingle(hit: AlgoliaNativeRecord, recommendationIdentifier: AlgoliaProductRecommendationIdentifier): ProductRecommendation {
229
+
230
+ const product = this.parseSearchResultItem(hit);
231
+
229
232
  return {
230
233
  recommendationIdentifier,
231
- product: {
232
- key: hit.objectID,
233
- },
234
+ recommendationReturnType: 'productSearchResultItem',
235
+ product: product,
234
236
  } satisfies ProductRecommendation;
235
237
  }
238
+
239
+
240
+ protected parseSearchResultItem(body: AlgoliaNativeRecord) {
241
+ const product = {
242
+ identifier: { key: body.objectID },
243
+ name: body.name || body.objectID,
244
+ slug: body.slug || body.objectID,
245
+ variants: [ ... (body.variants || []) ].map(variant => this.parseVariant(variant, body)),
246
+ } satisfies ProductSearchResultItem;
247
+
248
+ return product;
249
+ }
250
+
251
+ protected parseVariant(variant: AlgoliaNativeVariant, product: AlgoliaNativeRecord): ProductSearchResultItemVariant {
252
+ const result = ProductSearchResultItemVariantSchema.parse({
253
+ variant: {
254
+ sku: variant.sku
255
+ },
256
+ image: ImageSchema.parse({
257
+ sourceUrl: variant.image,
258
+ altText: product.name || '',
259
+ })
260
+ } satisfies Partial<ProductSearchResultItemVariant>);
261
+
262
+ return result;
263
+ }
236
264
  }
@@ -25,19 +25,8 @@ import {
25
25
  } from '@reactionary/core';
26
26
  import { algoliasearch, type SearchResponse } from 'algoliasearch';
27
27
  import type { AlgoliaConfiguration } from '../schema/configuration.schema.js';
28
- import type { AlgoliaProductSearchResult } from '../schema/search.schema.js';
28
+ import type { AlgoliaNativeRecord, AlgoliaNativeVariant, AlgoliaProductSearchResult } from '../schema/search.schema.js';
29
29
 
30
- interface AlgoliaNativeVariant {
31
- sku: string;
32
- image: string;
33
- }
34
-
35
- interface AlgoliaNativeRecord {
36
- objectID: string;
37
- slug?:string;
38
- name?: string;
39
- variants: Array<AlgoliaNativeVariant>;
40
- }
41
30
 
42
31
 
43
32
  export class AlgoliaProductSearchProvider extends ProductSearchProvider {
@@ -12,3 +12,16 @@ export const AlgoliaProductSearchResultSchema = ProductSearchResultSchema.extend
12
12
 
13
13
  export type AlgoliaProductSearchResult = z.infer<typeof AlgoliaProductSearchResultSchema>;
14
14
  export type AlgoliaProductSearchIdentifier = z.infer<typeof AlgoliaProductSearchIdentifierSchema>;
15
+
16
+
17
+ export interface AlgoliaNativeVariant {
18
+ sku: string;
19
+ image: string;
20
+ }
21
+
22
+ export interface AlgoliaNativeRecord {
23
+ objectID: string;
24
+ slug?:string;
25
+ name?: string;
26
+ variants: Array<AlgoliaNativeVariant>;
27
+ }
@@ -40,7 +40,7 @@ export class RequestContextTokenStore implements MedusaCustomStorage {
40
40
  this.context.session[SESSION_KEY] = {};
41
41
  }
42
42
  const retVal = this.context.session[SESSION_KEY]
43
- ? this.context.session[SESSION_KEY][this.keyPrefix + '_' + key] || null
43
+ ? (this.context.session[SESSION_KEY] as MedusaSession)[this.keyPrefix + '_' + key] || null
44
44
  : null;
45
45
  if (debug.enabled) {
46
46
  debug(
@@ -49,7 +49,7 @@ export class RequestContextTokenStore implements MedusaCustomStorage {
49
49
  }`
50
50
  );
51
51
  }
52
- return Promise.resolve(retVal);
52
+ return Promise.resolve(retVal as any);
53
53
  }
54
54
 
55
55
  setItem(key: string, value: string): Promise<void> {
@@ -63,7 +63,7 @@ export class RequestContextTokenStore implements MedusaCustomStorage {
63
63
  } - Value: ${value}`
64
64
  );
65
65
  }
66
- this.context.session[SESSION_KEY][this.keyPrefix + '_' + key] = value;
66
+ (this.context.session[SESSION_KEY] as MedusaSession)[this.keyPrefix + '_' + key] = value;
67
67
  return Promise.resolve();
68
68
  }
69
69
 
@@ -74,7 +74,7 @@ export class RequestContextTokenStore implements MedusaCustomStorage {
74
74
  if (debug.enabled) {
75
75
  debug(`Removing token item for key: ${this.keyPrefix + '_' + key}`);
76
76
  }
77
- delete this.context.session[SESSION_KEY][this.keyPrefix + '_' + key];
77
+ delete (this.context.session[SESSION_KEY] as MedusaSession)[this.keyPrefix + '_' + key];
78
78
  return Promise.resolve();
79
79
  }
80
80
  }
@@ -220,12 +220,12 @@ export class MedusaAPI {
220
220
 
221
221
  public getSessionData(): MedusaSession {
222
222
  return this.context.session[SESSION_KEY]
223
- ? this.context.session[SESSION_KEY]
223
+ ? this.context.session[SESSION_KEY] as Partial<MedusaSession>
224
224
  : MedusaSessionSchema.parse({});
225
225
  }
226
226
 
227
227
  public setSessionData(sessionData: Partial<MedusaSession>): void {
228
- const existingData = this.context.session[SESSION_KEY];
228
+ const existingData = this.context.session[SESSION_KEY] as Partial<MedusaSession>;
229
229
 
230
230
  this.context.session[SESSION_KEY] = {
231
231
  ...existingData,
@@ -1,14 +1,20 @@
1
+ import type { StoreProduct, StoreProductVariant } from '@medusajs/types';
1
2
  import {
2
- type Cache,
3
+ error,
4
+ ImageSchema,
3
5
  ProductRecommendationsProvider,
6
+ ProductSearchResultItemVariantSchema,
7
+ ProductVariantIdentifierSchema,
8
+ success,
9
+ type Cache,
10
+ type NotFoundError,
4
11
  type ProductRecommendation,
5
12
  type ProductRecommendationsByCollectionQuery,
13
+ type ProductSearchResultItem,
14
+ type ProductSearchResultItemVariant,
15
+ type ProductVariantIdentifier,
6
16
  type RequestContext,
7
- type Result,
8
- success, error,
9
- NotFoundErrorSchema,
10
- type NotFoundError,
11
- errorToString,
17
+ type Result
12
18
  } from '@reactionary/core';
13
19
  import createDebug from 'debug';
14
20
  import type { MedusaAPI } from '../core/client.js';
@@ -89,15 +95,17 @@ export class MedusaProductRecommendationsProvider extends ProductRecommendations
89
95
  // Map products to recommendations
90
96
  const recommendations: ProductRecommendation[] = [];
91
97
 
92
- for (const product of productsResponse.products) {
98
+
99
+
100
+ for (const productRes of productsResponse.products) {
101
+ const product = this.parseSearchResultItem(productRes);
93
102
  recommendations.push({
94
103
  recommendationIdentifier: {
95
- key: `${collection.id}_${product.id}`,
104
+ key: `${collection.id}_${productRes.id}`,
96
105
  algorithm: 'collection',
97
106
  },
98
- product: {
99
- key: product.id
100
- },
107
+ recommendationReturnType: 'productSearchResultItem',
108
+ product: product,
101
109
  });
102
110
  }
103
111
 
@@ -114,4 +122,43 @@ export class MedusaProductRecommendationsProvider extends ProductRecommendations
114
122
  return success([]);
115
123
  }
116
124
  }
125
+
126
+ protected parseSearchResultItem(_body: StoreProduct) {
127
+ const heroVariant = _body.variants?.[0];
128
+ const identifier = { key: _body.id };
129
+ const slug = _body.handle;
130
+ const name = heroVariant?.title || _body.title;
131
+ const variants = [];
132
+ if (heroVariant) {
133
+ variants.push(this.parseVariant(heroVariant, _body));
134
+ }
135
+
136
+ const result = {
137
+ identifier,
138
+ name,
139
+ slug,
140
+ variants,
141
+ } satisfies ProductSearchResultItem;
142
+
143
+ return result;
144
+ }
145
+
146
+ protected parseVariant(
147
+ variant: StoreProductVariant,
148
+ product: StoreProduct
149
+ ): ProductSearchResultItemVariant {
150
+ const img = ImageSchema.parse({
151
+ sourceUrl: product.images?.[0].url ?? '',
152
+ altText: product.title || undefined,
153
+ });
154
+
155
+ return ProductSearchResultItemVariantSchema.parse({
156
+ variant: ProductVariantIdentifierSchema.parse({
157
+ sku: variant.sku || '',
158
+ } satisfies ProductVariantIdentifier),
159
+ image: img,
160
+ } satisfies Partial<ProductSearchResultItemVariant>);
161
+ }
162
+
163
+
117
164
  }
@@ -2,18 +2,17 @@ import {
2
2
  type Cache,
3
3
  ProductRecommendationsProvider,
4
4
  type ProductRecommendation,
5
- type ProductRecommendationAlgorithmFrequentlyBoughtTogetherQuery,
6
5
  type ProductRecommendationAlgorithmSimilarProductsQuery,
7
- type ProductRecommendationAlgorithmRelatedProductsQuery,
8
- type ProductRecommendationAlgorithmTrendingInCategoryQuery,
9
6
  type RequestContext,
7
+ type ProductSearchResultItem,
8
+ ImageSchema,
9
+ type ProductSearchResultItemVariant,
10
+ ProductSearchResultItemVariantSchema,
10
11
  } from '@reactionary/core';
11
12
  import { MeiliSearch, type Hits, type RecordAny, type SearchParams, type SearchResponse } from 'meilisearch';
12
13
  import type { MeilisearchConfiguration } from '../schema/configuration.schema.js';
14
+ import type { MeilisearchNativeRecord, MeilisearchNativeVariant } from '../schema/index.js';
13
15
 
14
- interface MeilisearchRecommendHit {
15
- objectID: string;
16
- }
17
16
 
18
17
  /**
19
18
  * MeilisearchProductRecommendationsProvider
@@ -57,7 +56,7 @@ export class MeilisearchProductRecommendationsProvider extends ProductRecommenda
57
56
  limit: query.numberOfRecommendations,
58
57
  };
59
58
 
60
- const response = await index.searchSimilarDocuments<MeilisearchRecommendHit>({
59
+ const response = await index.searchSimilarDocuments<MeilisearchNativeRecord>({
61
60
  id: query.sourceProduct.key,
62
61
  limit: query.numberOfRecommendations,
63
62
  embedder: this.config.useAIEmbedding,
@@ -75,15 +74,44 @@ export class MeilisearchProductRecommendationsProvider extends ProductRecommenda
75
74
  /**
76
75
  * Maps Meilisearch search results to ProductRecommendation format
77
76
  */
78
- protected parseRecommendations(recommendation: SearchResponse<MeilisearchRecommendHit>, algorithm: string): ProductRecommendation[] {
77
+ protected parseRecommendations(recommendation: SearchResponse<MeilisearchNativeRecord>, algorithm: string): ProductRecommendation[] {
78
+
79
+ const product = this.parseSearchResultItem(recommendation.hits[0] as MeilisearchNativeRecord);
79
80
  return recommendation.hits.map((hit) => ({
80
81
  recommendationIdentifier: {
81
82
  key: hit.objectID,
82
83
  algorithm,
83
84
  },
84
- product: {
85
- key: hit.objectID,
86
- },
85
+ recommendationReturnType: 'productSearchResultItem',
86
+ product: product
87
87
  }));
88
88
  }
89
+
90
+
91
+
92
+
93
+ protected parseSearchResultItem(body: MeilisearchNativeRecord) {
94
+ const product = {
95
+ identifier: { key: body.objectID },
96
+ name: body.name || body.objectID,
97
+ slug: body.slug || body.objectID,
98
+ variants: [...(body.variants || [])].map(variant => this.parseVariant(variant, body)),
99
+ } satisfies ProductSearchResultItem;
100
+
101
+ return product;
102
+ }
103
+
104
+ protected parseVariant(variant: MeilisearchNativeVariant, product: MeilisearchNativeRecord): ProductSearchResultItemVariant {
105
+ const result = ProductSearchResultItemVariantSchema.parse({
106
+ variant: {
107
+ sku: variant.sku
108
+ },
109
+ image: ImageSchema.parse({
110
+ sourceUrl: variant.image,
111
+ altText: product.name || '',
112
+ })
113
+ } satisfies Partial<ProductSearchResultItemVariant>);
114
+
115
+ return result;
116
+ }
89
117
  }
@@ -25,19 +25,7 @@ import {
25
25
  } from '@reactionary/core';
26
26
  import { MeiliSearch, type SearchParams, type SearchResponse } from 'meilisearch';
27
27
  import type { MeilisearchConfiguration } from '../schema/configuration.schema.js';
28
- import type { MeilisearchProductSearchResult } from '../schema/search.schema.js';
29
-
30
- interface MeilisearchNativeVariant {
31
- sku: string;
32
- image: string;
33
- }
34
-
35
- interface MeilisearchNativeRecord {
36
- objectID: string;
37
- slug?: string;
38
- name?: string;
39
- variants: Array<MeilisearchNativeVariant>;
40
- }
28
+ import type { MeilisearchNativeRecord, MeilisearchNativeVariant, MeilisearchProductSearchResult } from '../schema/search.schema.js';
41
29
 
42
30
 
43
31
  export class MeilisearchSearchProvider extends ProductSearchProvider {
@@ -12,3 +12,14 @@ export const MeilisearchProductSearchResultSchema = ProductSearchResultSchema.ex
12
12
 
13
13
  export type MeilisearchProductSearchResult = z.infer<typeof MeilisearchProductSearchResultSchema>;
14
14
  export type MeilisearchProductSearchIdentifier = z.infer<typeof MeilisearchProductSearchIdentifierSchema>;
15
+ export interface MeilisearchNativeVariant {
16
+ sku: string;
17
+ image: string;
18
+ }
19
+
20
+ export interface MeilisearchNativeRecord {
21
+ objectID: string;
22
+ slug?: string;
23
+ name?: string;
24
+ variants: Array<MeilisearchNativeVariant>;
25
+ }