@reactionary/source 0.2.15 → 0.2.16

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.
@@ -131,7 +131,7 @@ export const OrderSearchIdentifierSchema = z.looseObject({
131
131
  term: z.string().describe('The search term used to find orders. Not all providers may support term-based search for orders.'),
132
132
  partNumber: z.array(z.string()).optional().describe('An optional list part number to filter orders by specific products. Will be ANDed together.'),
133
133
  orderStatus: z.array(OrderStatusSchema).optional().describe('An optional list of order statuses to filter the search results.'),
134
- userId: IdentityIdentifierSchema.optional().describe('An optional user ID to filter orders by specific users. Mostly for b2b usecases with hierachial order access'),
134
+ user: IdentityIdentifierSchema.optional().describe('An optional user ID to filter orders by specific users. Mostly for b2b usecases with hierachial order access.'),
135
135
  startDate: z.string().optional().describe('An optional start date to filter orders from a specific date onwards. ISO8601'),
136
136
  endDate: z.string().optional().describe('An optional end date to filter orders up to a specific date. ISO8601'),
137
137
  filters: z.array(z.string()).describe('Additional filters applied to the search results.'),
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@reactionary/examples-node",
3
- "version": "0.2.15",
3
+ "version": "0.2.16",
4
4
  "main": "index.js",
5
5
  "types": "src/index.d.ts",
6
6
  "dependencies": {
7
- "@reactionary/core": "0.2.15",
8
- "@reactionary/provider-commercetools": "0.2.15",
9
- "@reactionary/provider-algolia": "0.2.15",
10
- "@reactionary/provider-medusa": "0.2.15",
11
- "@reactionary/provider-meilisearch": "0.2.15"
7
+ "@reactionary/core": "0.2.16",
8
+ "@reactionary/provider-commercetools": "0.2.16",
9
+ "@reactionary/provider-algolia": "0.2.16",
10
+ "@reactionary/provider-medusa": "0.2.16",
11
+ "@reactionary/provider-meilisearch": "0.2.16"
12
12
  },
13
13
  "type": "module"
14
14
  }
@@ -5,10 +5,12 @@ import type { ProductSearchQueryCreateNavigationFilter } from '@reactionary/core
5
5
 
6
6
  const testData = {
7
7
  searchTerm: '',
8
- sku: '0766623301831'
8
+ sku: '0766623301831',
9
+ customerName: 'Eileen Harvey',
10
+ email: 'eileen.harvey@example.com',
9
11
  };
10
12
 
11
- describe.each([PrimaryProvider.COMMERCETOOLS])(
13
+ describe.each([PrimaryProvider.MEILISEARCH])(
12
14
  'Order Search Capability - %s',
13
15
  (provider) => {
14
16
  let client: ReturnType<typeof createClient>;
@@ -36,14 +38,12 @@ describe.each([PrimaryProvider.COMMERCETOOLS])(
36
38
  });
37
39
 
38
40
  it('can be called by guest users', async () => {
39
- const updatedCart = await client.cart.add(
40
- {
41
- quantity: 1,
42
- variant: {
43
- sku: testData.sku
44
- },
45
- }
46
- );
41
+ const updatedCart = await client.cart.add({
42
+ quantity: 1,
43
+ variant: {
44
+ sku: testData.sku,
45
+ },
46
+ });
47
47
  const identity = await client.identity.getSelf({});
48
48
 
49
49
  if (!identity.success) {
@@ -70,14 +70,13 @@ describe.each([PrimaryProvider.COMMERCETOOLS])(
70
70
 
71
71
  it('can be called by an authenticated user', async () => {
72
72
  const time = new Date().getTime();
73
- const identity = await client.identity.register(
74
- {
75
- username: `test-user+${time}@example.com`,
76
- password: 'love2test',
77
- }
78
- );
73
+ const identity = await client.identity.register({
74
+ username: `test-user+${time}@example.com`,
75
+ password: 'love2test',
76
+ });
79
77
 
80
78
  if (!identity.success) {
79
+
81
80
  assert.fail();
82
81
  }
83
82
 
@@ -96,64 +95,171 @@ describe.each([PrimaryProvider.COMMERCETOOLS])(
96
95
  if (!result.success) {
97
96
  assert.fail(JSON.stringify(result.error));
98
97
  }
99
-
100
- });
101
- it.skip('can filter by startDate', async () => {
102
- /** TODO */
103
98
  });
104
99
 
105
- it.skip('can filter by endDate', async () => {
106
- /** TODO */
107
- });
100
+ describe('filters', () => {
108
101
 
109
- it.skip('can filter by orderStatus', async () => {
110
- /** TODO */
111
- });
112
102
 
113
- it.skip('can filter by multiple orderStatuses', async () => {
114
- /** TODO */
115
- });
103
+ beforeEach(async () => {
104
+ const time = new Date().getTime();
105
+ const identity = await client.identity.login({ username: testData.email,
106
+ password: 'Test1234!'
107
+ });
116
108
 
117
- it.skip('can filter by partNumber', async () => {
118
- /** TODO */
119
- });
109
+ if (!identity.success) {
110
+ assert.fail();
111
+ }
112
+ expect(identity.value.type).toBe('Registered');
113
+ });
120
114
 
115
+ it('has some test orders', async () => {
116
+ const result = await client.orderSearch.queryByTerm({
117
+ search: {
118
+ term: '',
119
+ paginationOptions: {
120
+ pageNumber: 1,
121
+ pageSize: 10,
122
+ },
123
+ filters: [],
124
+ },
125
+ });
121
126
 
122
- it.skip('can page the resultset', async () => {
123
- const result = await client.orderSearch.queryByTerm({
124
- search: {
125
- term: testData.searchTerm,
126
- paginationOptions: {
127
- pageNumber: 1,
128
- pageSize: 2,
127
+ if (!result.success) {
128
+ assert.fail(JSON.stringify(result.error));
129
+ }
130
+
131
+ expect(result.value.items.length).toBeGreaterThan(0);
132
+ });
133
+
134
+ it('can filter by part number', async () => {
135
+
136
+ const result = await client.orderSearch.queryByTerm({
137
+ search: {
138
+ term: '',
139
+ paginationOptions: {
140
+ pageNumber: 1,
141
+ pageSize: 10,
142
+ },
143
+ filters: [],
129
144
  },
130
- filters: [],
131
- },
145
+ });
146
+
147
+ if (!result.success) {
148
+ assert.fail(JSON.stringify(result.error));
149
+ }
150
+
151
+
152
+ const result2 = await client.orderSearch.queryByTerm({
153
+ search: {
154
+ term: '',
155
+ partNumber: [testData.sku],
156
+ paginationOptions: {
157
+ pageNumber: 1,
158
+ pageSize: 10,
159
+ },
160
+ filters: [],
161
+ },
162
+ });
163
+
164
+ if (!result2.success) {
165
+ assert.fail(JSON.stringify(result2.error));
166
+ }
167
+ expect(result2.value.items.length).toBeGreaterThan(0);
168
+ expect(result2.value.totalCount).toBeLessThanOrEqual(result.value.totalCount);
132
169
  });
133
170
 
134
- if (!result.success) {
135
- assert.fail(JSON.stringify(result.error));
136
- }
171
+ it('can filter by search term', async () => {
172
+ const result = await client.orderSearch.queryByTerm({
173
+ search: {
174
+ term: 'cable',
175
+ paginationOptions: {
176
+ pageNumber: 1,
177
+ pageSize: 10,
178
+ },
179
+ filters: [],
180
+ },
181
+ });
137
182
 
138
- const result2 = await client.orderSearch.queryByTerm({
139
- search: {
140
- term: testData.searchTerm,
141
- paginationOptions: {
142
- pageNumber: 2,
143
- pageSize: 2,
183
+ if (!result.success) {
184
+ assert.fail(JSON.stringify(result.error));
185
+ }
186
+ expect(result.value.items.length).toBeGreaterThan(0);
187
+ });
188
+
189
+
190
+
191
+ it('can paginate results', async () => {
192
+ const result = await client.orderSearch.queryByTerm({
193
+ search: {
194
+ term: '',
195
+ paginationOptions: {
196
+ pageNumber: 1,
197
+ pageSize: 1,
198
+ },
199
+ filters: [],
144
200
  },
145
- filters: [],
146
- },
201
+ });
202
+
203
+ if (!result.success) {
204
+ assert.fail(JSON.stringify(result.error));
205
+ }
206
+ expect(result.value.items.length).toBe(1);
207
+
208
+ const result2 = await client.orderSearch.queryByTerm({
209
+ search: {
210
+ term: '',
211
+ paginationOptions: {
212
+ pageNumber: 2,
213
+ pageSize: 1,
214
+ },
215
+ filters: [],
216
+ },
217
+ });
218
+
219
+ if (!result2.success) {
220
+ assert.fail(JSON.stringify(result2.error));
221
+ }
222
+ expect(result2.value.items.length).toBe(1);
223
+ expect(result2.value.items[0].identifier.key).not.toBe(
224
+ result.value.items[0].identifier.key
225
+ );
147
226
  });
148
227
 
149
- if (!result2.success) {
150
- assert.fail(JSON.stringify(result2.error));
151
- }
228
+ it('can filter by order status', async () => {
229
+ const result = await client.orderSearch.queryByTerm({
230
+ search: {
231
+ term: '',
232
+ orderStatus: ['Shipped'],
233
+ paginationOptions: {
234
+ pageNumber: 1,
235
+ pageSize: 10,
236
+ },
237
+ filters: [],
238
+ },
239
+ });
152
240
 
153
- expect(result.value.items[0].identifier).not.toBe(
154
- result2.value.items[0].identifier
155
- );
241
+ if (!result.success) {
242
+ assert.fail(JSON.stringify(result.error));
243
+ }
244
+ expect(result.value.items.length).toBeGreaterThan(0);
245
+
246
+ const result2 = await client.orderSearch.queryByTerm({
247
+ search: {
248
+ term: '',
249
+ orderStatus: ['Cancelled'],
250
+ paginationOptions: {
251
+ pageNumber: 1,
252
+ pageSize: 10,
253
+ },
254
+ filters: [],
255
+ },
256
+ });
257
+
258
+ if (!result2.success) {
259
+ assert.fail(JSON.stringify(result2.error));
260
+ }
261
+ expect(result2.value.items.length).toBe(0);
262
+ });
156
263
  });
157
264
  }
158
-
159
265
  );
@@ -23,6 +23,7 @@ export function getMeilisearchTestConfiguration() {
23
23
  apiUrl: process.env['MEILISEARCH_API_URL'] || '',
24
24
  indexName: process.env['MEILISEARCH_INDEX'] || '',
25
25
  useAIEmbedding: process.env['MEILISEARCH_USE_AI_EMBEDDING'] || undefined,
26
+ orderIndexName: process.env['MEILISEARCH_ORDER_INDEX'] || 'order',
26
27
  };
27
28
  }
28
29
 
@@ -90,6 +91,7 @@ export function createClient(provider: PrimaryProvider) {
90
91
  order: true,
91
92
  price: true,
92
93
  productSearch: true,
94
+ orderSearch: true,
93
95
  store: true,
94
96
  profile: true
95
97
  })
@@ -110,6 +112,7 @@ export function createClient(provider: PrimaryProvider) {
110
112
  order: true,
111
113
  price: true,
112
114
  productSearch: true,
115
+ orderSearch: true,
113
116
  store: true,
114
117
  profile: true,
115
118
  })
@@ -129,6 +132,13 @@ export function createClient(provider: PrimaryProvider) {
129
132
  builder = builder.withCapability(
130
133
  withMeilisearchCapabilities(getMeilisearchTestConfiguration(), {
131
134
  productSearch: true,
135
+ orderSearch: true,
136
+ }),
137
+ );
138
+ builder = builder.withCapability(
139
+ withMedusaCapabilities(getMedusaTestConfiguration(), {
140
+ cart: true,
141
+ identity: true,
132
142
  })
133
143
  );
134
144
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reactionary/source",
3
- "version": "0.2.15",
3
+ "version": "0.2.16",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "dependencies": {
@@ -70,8 +70,8 @@ export class CommercetoolsOrderSearchProvider extends OrderSearchProvider {
70
70
  }
71
71
  }
72
72
 
73
- if (payload.search.userId) {
74
- where.push(`customerId="${payload.search.userId}"`);
73
+ if (payload.search.user && payload.search.user.userId) {
74
+ where.push(`customerId="${payload.search.user.userId}"`);
75
75
  }
76
76
 
77
77
  if (payload.search.orderStatus) {
@@ -67,9 +67,10 @@ export class MedusaOrderSearchProvider extends OrderSearchProvider {
67
67
  if (payload.search.endDate) {
68
68
  debug('Searching orders by end date is not supported in Medusa');
69
69
  }
70
- if (payload.search.userId) {
70
+ /*
71
+ if (payload.search.user && payload.search.user.userId) {
71
72
  debug('Searching orders by customer ID is not supported in Medusa');
72
- }
73
+ } */
73
74
  const statusFilter: MedusaOrderStatus[] = (payload.search.orderStatus ?? []).map((status) => {
74
75
  let retStatus: MedusaOrderStatus = 'draft';
75
76
  if (status === 'AwaitingPayment') {
@@ -1,5 +1,6 @@
1
1
  import type { Cache, ClientFromCapabilities, RequestContext } from "@reactionary/core";
2
2
  import { MeilisearchSearchProvider } from "../providers/product-search.provider.js";
3
+ import { MeilisearchOrderSearchProvider } from "../providers/order-search.provider.js";
3
4
  import type { MeilisearchCapabilities } from "../schema/capabilities.schema.js";
4
5
  import type { MeilisearchConfiguration } from "../schema/configuration.schema.js";
5
6
 
@@ -11,6 +12,10 @@ export function withMeilisearchCapabilities<T extends MeilisearchCapabilities>(c
11
12
  client.productSearch = new MeilisearchSearchProvider(configuration, cache, context);
12
13
  }
13
14
 
15
+ if (capabilities.orderSearch) {
16
+ client.orderSearch = new MeilisearchOrderSearchProvider(configuration, cache, context);
17
+ }
18
+
14
19
  return client;
15
20
  };
16
21
  }
@@ -1,5 +1,6 @@
1
1
  export * from './core/initialize.js';
2
2
  export * from './providers/product-search.provider.js';
3
+ export * from './providers/order-search.provider.js';
3
4
 
4
5
  export * from './schema/configuration.schema.js';
5
6
  export * from './schema/capabilities.schema.js';
@@ -0,0 +1,222 @@
1
+ import {
2
+ type Cache,
3
+ type OrderSearchQueryByTerm,
4
+ OrderSearchQueryByTermSchema,
5
+ type OrderSearchResult,
6
+ type OrderSearchResultItem,
7
+ OrderSearchResultSchema,
8
+ OrderSearchProvider,
9
+ Reactionary,
10
+ type RequestContext,
11
+ type Result,
12
+ success,
13
+ type OrderStatus,
14
+ type Address,
15
+ type IdentityIdentifier,
16
+ type MonetaryAmount,
17
+ type Currency,
18
+ type OrderSearchIdentifier,
19
+ AddressIdentifierSchema,
20
+ type AddressIdentifier,
21
+ type OrderInventoryStatus,
22
+ } from '@reactionary/core';
23
+ import { MeiliSearch, type SearchParams, type SearchResponse } from 'meilisearch';
24
+ import type { MeilisearchConfiguration } from '../schema/configuration.schema.js';
25
+
26
+ interface MeilisearchNativeOrderAddress {
27
+ address1: string;
28
+ address2: string;
29
+ city: string;
30
+ postalCode: string;
31
+ country: string;
32
+ }
33
+
34
+ interface MeilisearchNativeOrderRecord {
35
+ orderIdentifier: string;
36
+ userIdentifier: string;
37
+ customerName: string;
38
+ shippingAddress: MeilisearchNativeOrderAddress;
39
+ orderDate: string;
40
+ orderStatus: string;
41
+ inventoryStatus: string;
42
+ totalAmount: number;
43
+ currency: string;
44
+ }
45
+
46
+ export class MeilisearchOrderSearchProvider extends OrderSearchProvider {
47
+ protected config: MeilisearchConfiguration;
48
+
49
+ constructor(config: MeilisearchConfiguration, cache: Cache, context: RequestContext) {
50
+ super(cache, context);
51
+ this.config = config;
52
+ }
53
+
54
+ @Reactionary({
55
+ inputSchema: OrderSearchQueryByTermSchema,
56
+ outputSchema: OrderSearchResultSchema,
57
+ })
58
+ public async queryByTerm(payload: OrderSearchQueryByTerm): Promise<Result<OrderSearchResult>> {
59
+ const client = new MeiliSearch({
60
+ host: this.config.apiUrl,
61
+ apiKey: this.config.apiKey,
62
+ });
63
+
64
+ const index = client.index(this.config.orderIndexName);
65
+
66
+ const filters: string[] = [];
67
+
68
+ // Add status filter
69
+ if (payload.search.orderStatus && payload.search.orderStatus.length > 0) {
70
+ const statusFilters = payload.search.orderStatus
71
+ .map((status) => `orderStatus = "${this.mapOrderStatus(status)}"`)
72
+ .join(' OR ');
73
+ filters.push(`(${statusFilters})`);
74
+ }
75
+
76
+ // Add user ID filter for B2B use cases with hierarchical order access
77
+ if (payload.search.user) {
78
+ filters.push(`userIdentifier = "${payload.search.user.userId}"`);
79
+ }
80
+
81
+ // Add date range filters
82
+ if (payload.search.startDate) {
83
+ filters.push(`orderDate >= ${new Date(payload.search.startDate).getTime()}`);
84
+ }
85
+ if (payload.search.endDate) {
86
+ filters.push(`orderDate <= ${new Date(payload.search.endDate).getTime()}`);
87
+ }
88
+
89
+
90
+ if (payload.search.partNumber && payload.search.partNumber.length > 0) {
91
+ const partNumberFilters = payload.search.partNumber
92
+ .map((partNumber) => `items.sku = "${partNumber}"`)
93
+ .join(' OR ');
94
+ filters.push(`(${partNumberFilters})`);
95
+ }
96
+
97
+ const searchOptions: SearchParams = {
98
+ offset:
99
+ (payload.search.paginationOptions.pageNumber - 1) *
100
+ payload.search.paginationOptions.pageSize,
101
+ limit: payload.search.paginationOptions.pageSize,
102
+ filter: filters.length > 0 ? filters.join(' AND ') : undefined,
103
+ };
104
+
105
+ const remote = await index.search<MeilisearchNativeOrderRecord>(
106
+ payload.search.term || '',
107
+ searchOptions
108
+ );
109
+
110
+ const result = this.parsePaginatedResult(remote, payload) as OrderSearchResult;
111
+
112
+ return success(result);
113
+ }
114
+
115
+ protected mapOrderStatus(status: OrderStatus): string {
116
+ // Map from Reactionary OrderStatus to Meilisearch native status
117
+ const statusMap: Record<OrderStatus, string> = {
118
+ AwaitingPayment: 'awaiting_payment',
119
+ ReleasedToFulfillment: 'released_to_fulfillment',
120
+ Shipped: 'shipped',
121
+ Cancelled: 'cancelled',
122
+ };
123
+ return statusMap[status] || status;
124
+ }
125
+
126
+ protected mapFromNativeOrderStatus(nativeStatus: string): OrderStatus {
127
+ // Map from Meilisearch native status to Reactionary OrderStatus
128
+ const statusMap: Record<string, OrderStatus> = {
129
+ awaiting_payment: 'AwaitingPayment',
130
+ released_to_fulfillment: 'ReleasedToFulfillment',
131
+ shipped: 'Shipped',
132
+ cancelled: 'Cancelled',
133
+ };
134
+ return statusMap[nativeStatus] || 'AwaitingPayment';
135
+ }
136
+
137
+ protected mapFromNativeInventoryStatus(nativeStatus: string): OrderInventoryStatus {
138
+ // Map from Meilisearch native status to Reactionary OrderInventoryStatus
139
+ const statusMap: Record<string, OrderInventoryStatus> = {
140
+ not_allocated: 'NotAllocated',
141
+ allocated: 'Allocated',
142
+ preordered: 'Preordered',
143
+ backordered: 'Backordered',
144
+ };
145
+ return statusMap[nativeStatus] || 'NotAllocated';
146
+ }
147
+
148
+ protected composeAddressFromNativeAddress(
149
+ nativeAddress: MeilisearchNativeOrderAddress
150
+ ): Address {
151
+ return {
152
+ identifier: AddressIdentifierSchema.parse({
153
+ nickName: 'shipping',
154
+ } satisfies AddressIdentifier),
155
+ firstName: '',
156
+ lastName: '',
157
+ streetAddress: nativeAddress.address1,
158
+ streetNumber: nativeAddress.address2,
159
+ city: nativeAddress.city,
160
+ postalCode: nativeAddress.postalCode,
161
+ countryCode: nativeAddress.country,
162
+ region: '',
163
+ };
164
+ }
165
+
166
+ protected parseSingle(body: MeilisearchNativeOrderRecord): OrderSearchResultItem {
167
+ const identifier = { key: body.orderIdentifier };
168
+ const userId: IdentityIdentifier = {
169
+ userId: body.userIdentifier,
170
+ };
171
+ const customerName = body.customerName;
172
+ const shippingAddress = this.composeAddressFromNativeAddress(body.shippingAddress);
173
+ const orderDate = body.orderDate;
174
+ const orderStatus = this.mapFromNativeOrderStatus(body.orderStatus);
175
+ const inventoryStatus = this.mapFromNativeInventoryStatus(body.inventoryStatus);
176
+
177
+ const totalAmount: MonetaryAmount = {
178
+ currency: (body.currency || this.context.languageContext.currencyCode) as Currency,
179
+ value: body.totalAmount,
180
+ };
181
+
182
+ const order = {
183
+ identifier,
184
+ userId,
185
+ customerName,
186
+ shippingAddress,
187
+ orderDate,
188
+ orderStatus,
189
+ inventoryStatus,
190
+ totalAmount,
191
+ } satisfies OrderSearchResultItem;
192
+
193
+ return order;
194
+ }
195
+
196
+ protected parsePaginatedResult(
197
+ body: SearchResponse<MeilisearchNativeOrderRecord>,
198
+ query: OrderSearchQueryByTerm
199
+ ): OrderSearchResult {
200
+ const identifier = {
201
+ ...query.search,
202
+ } satisfies OrderSearchIdentifier;
203
+
204
+ const orders: OrderSearchResultItem[] = body.hits.map((hit) => {
205
+ return this.parseSingle(hit);
206
+ });
207
+
208
+ const totalCount = body.estimatedTotalHits || body.hits.length;
209
+ const totalPages = Math.ceil(totalCount / (body.limit || 1));
210
+
211
+ const result = {
212
+ identifier,
213
+ pageNumber: Math.floor((body.offset || 0) / (body.limit || 1) ) + 1,
214
+ pageSize: body.limit || orders.length,
215
+ totalCount,
216
+ totalPages,
217
+ items: orders,
218
+ } satisfies OrderSearchResult;
219
+
220
+ return result;
221
+ }
222
+ }
@@ -3,6 +3,7 @@ import type { z } from 'zod';
3
3
 
4
4
  export const MeilisearchCapabilitiesSchema = CapabilitiesSchema.pick({
5
5
  productSearch: true,
6
+ orderSearch: true,
6
7
  analytics: true
7
8
  }).partial();
8
9
 
@@ -4,6 +4,7 @@ export const MeilisearchConfigurationSchema = z.looseObject({
4
4
  apiUrl: z.string(),
5
5
  apiKey: z.string(),
6
6
  indexName: z.string(),
7
+ orderIndexName: z.string(),
7
8
  useAIEmbedding: z.string().optional()
8
9
  });
9
10