@labdigital/commercetools-mock 2.30.0 → 2.31.0
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/dist/index.cjs +374 -203
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +374 -203
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/lib/productSearchFilter.test.ts +328 -0
- package/src/lib/productSearchFilter.ts +329 -0
- package/src/product-search.ts +12 -2
- package/src/repositories/order/actions.ts +9 -0
- package/src/services/order.test.ts +14 -0
package/package.json
CHANGED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { ProductProjection, _SearchQuery } from "@commercetools/platform-sdk";
|
|
2
|
+
import { describe, expect, test } from "vitest";
|
|
3
|
+
import { cloneObject } from "~src/helpers";
|
|
4
|
+
import { parseSearchQuery } from "./productSearchFilter";
|
|
5
|
+
|
|
6
|
+
describe("Product search filter", () => {
|
|
7
|
+
const exampleProduct: ProductProjection = {
|
|
8
|
+
id: "7401d82f-1378-47ba-996a-85beeb87ac87",
|
|
9
|
+
version: 2,
|
|
10
|
+
createdAt: "2022-07-22T10:02:40.851Z",
|
|
11
|
+
lastModifiedAt: "2022-07-22T10:02:44.427Z",
|
|
12
|
+
key: "test-product",
|
|
13
|
+
productType: {
|
|
14
|
+
typeId: "product-type",
|
|
15
|
+
id: "b9b4b426-938b-4ccb-9f36-c6f933e8446e",
|
|
16
|
+
},
|
|
17
|
+
name: {
|
|
18
|
+
"nl-NL": "test",
|
|
19
|
+
"en-US": "my english test",
|
|
20
|
+
},
|
|
21
|
+
slug: {
|
|
22
|
+
"nl-NL": "test",
|
|
23
|
+
"en-US": "test",
|
|
24
|
+
},
|
|
25
|
+
variants: [],
|
|
26
|
+
searchKeywords: {},
|
|
27
|
+
categories: [],
|
|
28
|
+
masterVariant: {
|
|
29
|
+
id: 1,
|
|
30
|
+
sku: "MYSKU",
|
|
31
|
+
attributes: [
|
|
32
|
+
{
|
|
33
|
+
name: "Country",
|
|
34
|
+
value: {
|
|
35
|
+
key: "NL",
|
|
36
|
+
label: {
|
|
37
|
+
de: "niederlande",
|
|
38
|
+
en: "netherlands",
|
|
39
|
+
nl: "nederland",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "number",
|
|
45
|
+
value: 4,
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
prices: [
|
|
49
|
+
{
|
|
50
|
+
id: "dummy-uuid",
|
|
51
|
+
value: {
|
|
52
|
+
type: "centPrecision",
|
|
53
|
+
currencyCode: "EUR",
|
|
54
|
+
centAmount: 1789,
|
|
55
|
+
fractionDigits: 2,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const match = (filterObject: _SearchQuery, product?: ProductProjection) => {
|
|
63
|
+
const matchFunc = parseSearchQuery(filterObject);
|
|
64
|
+
const clone = cloneObject(product ?? exampleProduct);
|
|
65
|
+
return {
|
|
66
|
+
isMatch: matchFunc(clone, false),
|
|
67
|
+
product: clone,
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
test("by product key", async () => {
|
|
72
|
+
expect(
|
|
73
|
+
match({
|
|
74
|
+
exists: {
|
|
75
|
+
field: "key",
|
|
76
|
+
},
|
|
77
|
+
}).isMatch,
|
|
78
|
+
).toBeTruthy();
|
|
79
|
+
|
|
80
|
+
expect(
|
|
81
|
+
match({
|
|
82
|
+
not: {
|
|
83
|
+
exists: {
|
|
84
|
+
field: "key",
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
}).isMatch,
|
|
88
|
+
).toBeFalsy();
|
|
89
|
+
|
|
90
|
+
expect(
|
|
91
|
+
match({
|
|
92
|
+
exact: {
|
|
93
|
+
field: "key",
|
|
94
|
+
value: "test-product",
|
|
95
|
+
},
|
|
96
|
+
}).isMatch,
|
|
97
|
+
).toBeTruthy();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("by product type id", async () => {
|
|
101
|
+
expect(
|
|
102
|
+
match({
|
|
103
|
+
exact: {
|
|
104
|
+
field: "productType.id",
|
|
105
|
+
value: "b9b4b426-938b-4ccb-9f36-c6f933e8446e",
|
|
106
|
+
},
|
|
107
|
+
}).isMatch,
|
|
108
|
+
).toBeTruthy();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("by variant SKU", async () => {
|
|
112
|
+
expect(
|
|
113
|
+
match({
|
|
114
|
+
exists: {
|
|
115
|
+
field: "variants.sku",
|
|
116
|
+
},
|
|
117
|
+
}).isMatch,
|
|
118
|
+
).toBeTruthy();
|
|
119
|
+
|
|
120
|
+
expect(
|
|
121
|
+
match({
|
|
122
|
+
not: {
|
|
123
|
+
exists: {
|
|
124
|
+
field: "variants.sku",
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
}).isMatch,
|
|
128
|
+
).toBeFalsy();
|
|
129
|
+
|
|
130
|
+
expect(
|
|
131
|
+
match({
|
|
132
|
+
exact: {
|
|
133
|
+
field: "variants.sku",
|
|
134
|
+
value: "MYSKU",
|
|
135
|
+
},
|
|
136
|
+
}).isMatch,
|
|
137
|
+
).toBeTruthy();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("by attribute value", async () => {
|
|
141
|
+
expect(
|
|
142
|
+
match({
|
|
143
|
+
exact: {
|
|
144
|
+
field: "variants.attributes.number",
|
|
145
|
+
value: 4,
|
|
146
|
+
},
|
|
147
|
+
}).isMatch,
|
|
148
|
+
).toBeTruthy();
|
|
149
|
+
|
|
150
|
+
expect(
|
|
151
|
+
match({
|
|
152
|
+
or: [
|
|
153
|
+
{
|
|
154
|
+
exact: {
|
|
155
|
+
field: "variants.attributes.number",
|
|
156
|
+
value: 3,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
exact: {
|
|
161
|
+
field: "variants.attributes.number",
|
|
162
|
+
value: 4,
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
}).isMatch,
|
|
167
|
+
).toBeTruthy();
|
|
168
|
+
|
|
169
|
+
expect(
|
|
170
|
+
match({
|
|
171
|
+
exact: {
|
|
172
|
+
field: "variants.attributes.number",
|
|
173
|
+
value: 5,
|
|
174
|
+
},
|
|
175
|
+
}).isMatch,
|
|
176
|
+
).toBeFalsy();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("by attribute range", async () => {
|
|
180
|
+
expect(
|
|
181
|
+
match({
|
|
182
|
+
range: {
|
|
183
|
+
field: "variants.attributes.number",
|
|
184
|
+
gt: 0,
|
|
185
|
+
lt: 5,
|
|
186
|
+
},
|
|
187
|
+
}).isMatch,
|
|
188
|
+
).toBeTruthy();
|
|
189
|
+
|
|
190
|
+
expect(
|
|
191
|
+
match({
|
|
192
|
+
range: {
|
|
193
|
+
field: "variants.attributes.number",
|
|
194
|
+
gt: 4,
|
|
195
|
+
lt: 10,
|
|
196
|
+
},
|
|
197
|
+
}).isMatch,
|
|
198
|
+
).toBeFalsy();
|
|
199
|
+
|
|
200
|
+
expect(
|
|
201
|
+
match({
|
|
202
|
+
range: {
|
|
203
|
+
field: "variants.attributes.number",
|
|
204
|
+
gte: 4,
|
|
205
|
+
lte: 10,
|
|
206
|
+
},
|
|
207
|
+
}).isMatch,
|
|
208
|
+
).toBeTruthy();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("by attribute enum key", async () => {
|
|
212
|
+
expect(
|
|
213
|
+
match({
|
|
214
|
+
exact: {
|
|
215
|
+
field: "variants.attributes.Country.key",
|
|
216
|
+
value: "NL",
|
|
217
|
+
},
|
|
218
|
+
}).isMatch,
|
|
219
|
+
).toBeTruthy();
|
|
220
|
+
|
|
221
|
+
expect(
|
|
222
|
+
match({
|
|
223
|
+
exact: {
|
|
224
|
+
field: "variants.attributes.Country.key",
|
|
225
|
+
value: "DE",
|
|
226
|
+
},
|
|
227
|
+
}).isMatch,
|
|
228
|
+
).toBeFalsy();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("by attribute text value", async () => {
|
|
232
|
+
expect(
|
|
233
|
+
match({
|
|
234
|
+
wildcard: {
|
|
235
|
+
field: "name",
|
|
236
|
+
value: "*test*",
|
|
237
|
+
language: "nl-NL",
|
|
238
|
+
caseInsensitive: true,
|
|
239
|
+
},
|
|
240
|
+
}).isMatch,
|
|
241
|
+
).toBeTruthy();
|
|
242
|
+
|
|
243
|
+
expect(
|
|
244
|
+
match({
|
|
245
|
+
wildcard: {
|
|
246
|
+
field: "name",
|
|
247
|
+
value: "*other*",
|
|
248
|
+
language: "nl-NL",
|
|
249
|
+
caseInsensitive: true,
|
|
250
|
+
},
|
|
251
|
+
}).isMatch,
|
|
252
|
+
).toBeFalsy();
|
|
253
|
+
|
|
254
|
+
expect(
|
|
255
|
+
match({
|
|
256
|
+
wildcard: {
|
|
257
|
+
field: "name",
|
|
258
|
+
value: "*Test*",
|
|
259
|
+
language: "nl-NL",
|
|
260
|
+
caseInsensitive: false,
|
|
261
|
+
},
|
|
262
|
+
}).isMatch,
|
|
263
|
+
).toBeFalsy();
|
|
264
|
+
|
|
265
|
+
expect(
|
|
266
|
+
match({
|
|
267
|
+
wildcard: {
|
|
268
|
+
field: "name",
|
|
269
|
+
value: "*english Test*",
|
|
270
|
+
language: "en-US",
|
|
271
|
+
caseInsensitive: true,
|
|
272
|
+
},
|
|
273
|
+
}).isMatch,
|
|
274
|
+
).toBeTruthy();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("by price range", async () => {
|
|
278
|
+
expect(
|
|
279
|
+
match({
|
|
280
|
+
range: {
|
|
281
|
+
field: "variants.prices.currentCentAmount",
|
|
282
|
+
gte: 1500,
|
|
283
|
+
lte: 2000,
|
|
284
|
+
},
|
|
285
|
+
}).isMatch,
|
|
286
|
+
).toBeTruthy();
|
|
287
|
+
|
|
288
|
+
expect(
|
|
289
|
+
match({
|
|
290
|
+
range: {
|
|
291
|
+
field: "variants.prices.currentCentAmount",
|
|
292
|
+
gt: 1800,
|
|
293
|
+
lte: 2000,
|
|
294
|
+
},
|
|
295
|
+
}).isMatch,
|
|
296
|
+
).toBeFalsy();
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("by price range - or", async () => {
|
|
300
|
+
expect(
|
|
301
|
+
match({
|
|
302
|
+
or: [
|
|
303
|
+
{
|
|
304
|
+
range: {
|
|
305
|
+
field: "variants.prices.currentCentAmount",
|
|
306
|
+
gte: 2,
|
|
307
|
+
lte: 1500,
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
range: {
|
|
312
|
+
field: "variants.prices.currentCentAmount",
|
|
313
|
+
gte: 1500,
|
|
314
|
+
lte: 3000,
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
range: {
|
|
319
|
+
field: "variants.prices.currentCentAmount",
|
|
320
|
+
gte: 3000,
|
|
321
|
+
lte: 4000,
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
],
|
|
325
|
+
}).isMatch,
|
|
326
|
+
).toBeTruthy();
|
|
327
|
+
});
|
|
328
|
+
});
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ProductProjection,
|
|
3
|
+
ProductVariant,
|
|
4
|
+
SearchAndExpression,
|
|
5
|
+
SearchDateRangeExpression,
|
|
6
|
+
SearchDateTimeRangeExpression,
|
|
7
|
+
SearchExactExpression,
|
|
8
|
+
SearchExistsExpression,
|
|
9
|
+
SearchFilterExpression,
|
|
10
|
+
SearchFullTextExpression,
|
|
11
|
+
SearchFullTextPrefixExpression,
|
|
12
|
+
SearchLongRangeExpression,
|
|
13
|
+
SearchNotExpression,
|
|
14
|
+
SearchNumberRangeExpression,
|
|
15
|
+
SearchOrExpression,
|
|
16
|
+
SearchPrefixExpression,
|
|
17
|
+
SearchTimeRangeExpression,
|
|
18
|
+
SearchWildCardExpression,
|
|
19
|
+
_SearchQuery,
|
|
20
|
+
_SearchQueryExpression,
|
|
21
|
+
_SearchQueryExpressionValue,
|
|
22
|
+
} from "@commercetools/platform-sdk";
|
|
23
|
+
import { nestedLookup } from "~src/helpers";
|
|
24
|
+
import type { Writable } from "../types";
|
|
25
|
+
import { getVariants } from "./projectionSearchFilter";
|
|
26
|
+
|
|
27
|
+
type ProductSearchFilterFunc = (
|
|
28
|
+
p: Writable<ProductProjection>,
|
|
29
|
+
markMatchingVariants: boolean,
|
|
30
|
+
) => boolean;
|
|
31
|
+
|
|
32
|
+
// @TODO: Implement field boosting:
|
|
33
|
+
// https://docs.commercetools.com/merchant-center/advanced-product-search#boost-field
|
|
34
|
+
export const parseSearchQuery = (
|
|
35
|
+
searchQuery: _SearchQuery,
|
|
36
|
+
): ProductSearchFilterFunc => {
|
|
37
|
+
if (isSearchAndExpression(searchQuery)) {
|
|
38
|
+
return (obj, markMatchingVariant) =>
|
|
39
|
+
searchQuery.and.every((expr) => {
|
|
40
|
+
const filterFunc = parseSearchQuery(expr);
|
|
41
|
+
|
|
42
|
+
return filterFunc(obj, markMatchingVariant);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (isSearchOrExpression(searchQuery)) {
|
|
47
|
+
return (obj, markMatchingVariant) =>
|
|
48
|
+
searchQuery.or.some((expr) => {
|
|
49
|
+
const filterFunc = parseSearchQuery(expr);
|
|
50
|
+
|
|
51
|
+
return filterFunc(obj, markMatchingVariant);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (isSearchNotExpression(searchQuery)) {
|
|
56
|
+
return (obj, markMatchingVariant) =>
|
|
57
|
+
!parseSearchQuery(searchQuery.not)(obj, markMatchingVariant);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (isSearchFilterExpression(searchQuery)) {
|
|
61
|
+
// Matching resources of a query are checked for their relevancy to the search.
|
|
62
|
+
// The relevancy is expressed by an internal score.
|
|
63
|
+
// All expressions except filter expressions contribute to that score.
|
|
64
|
+
// All sub-expressions of a filter are implicitly connected with an and expression.
|
|
65
|
+
// NOTE: for now just implementing it like a AND expression
|
|
66
|
+
return (obj, markMatchingVariant) =>
|
|
67
|
+
searchQuery.filter.every((expr) => {
|
|
68
|
+
const filterFunc = parseSearchQuery(expr);
|
|
69
|
+
|
|
70
|
+
return filterFunc(obj, markMatchingVariant);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (isSearchRangeExpression(searchQuery)) {
|
|
75
|
+
const generateRangeMatchFunc = (value: any) => {
|
|
76
|
+
const rangeFilters = [];
|
|
77
|
+
|
|
78
|
+
if (searchQuery.range.gte) {
|
|
79
|
+
rangeFilters.push(value >= searchQuery.range.gte);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (searchQuery.range.gt) {
|
|
83
|
+
rangeFilters.push(value > searchQuery.range.gt);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (searchQuery.range.lte) {
|
|
87
|
+
rangeFilters.push(value <= searchQuery.range.lte);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (searchQuery.range.lt) {
|
|
91
|
+
rangeFilters.push(value < searchQuery.range.lt);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return rangeFilters.every((filter) => filter);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return generateFieldMatchFunc(generateRangeMatchFunc, searchQuery.range);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (isSearchExactExpression(searchQuery)) {
|
|
101
|
+
return generateFieldMatchFunc(
|
|
102
|
+
(value: any) => value === searchQuery.exact.value,
|
|
103
|
+
searchQuery.exact,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (isSearchExistsExpression(searchQuery)) {
|
|
108
|
+
return generateFieldMatchFunc((value: any) => !!value, searchQuery.exists);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (isSearchFullTextExpression(searchQuery)) {
|
|
112
|
+
// @TODO: Implement fulltext search, to fully support functionality offered by commercetools:
|
|
113
|
+
// https://docs.commercetools.com/api/search-query-language#fulltext
|
|
114
|
+
return generateFieldMatchFunc(
|
|
115
|
+
(value: any) => value.includes(searchQuery.fullText.value),
|
|
116
|
+
searchQuery.fullText,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (isSearchFullTextPrefixExpression(searchQuery)) {
|
|
121
|
+
// @TODO: Implement fulltext search, to fully support functionality offered by commercetools:
|
|
122
|
+
// https://docs.commercetools.com/api/search-query-language#fulltext
|
|
123
|
+
return generateFieldMatchFunc(
|
|
124
|
+
(value: any) => value.startsWith(searchQuery.fullTextPrefix.value),
|
|
125
|
+
searchQuery.fullTextPrefix,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (isSearchPrefixExpression(searchQuery)) {
|
|
130
|
+
return generateFieldMatchFunc(
|
|
131
|
+
(value: any) => value.startsWith(searchQuery.prefix.value),
|
|
132
|
+
searchQuery.prefix,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (isSearchWildCardExpression(searchQuery)) {
|
|
137
|
+
// @TODO: Fully implement wildcard search, as specified:
|
|
138
|
+
// https://docs.commercetools.com/api/search-query-language#fulltext
|
|
139
|
+
const generateWildcardMatchFunc = (value: any) => {
|
|
140
|
+
const wildCardValues = searchQuery.wildcard.value
|
|
141
|
+
.split("*")
|
|
142
|
+
.filter((v: string) => !!v);
|
|
143
|
+
|
|
144
|
+
if (searchQuery.wildcard.caseInsensitive) {
|
|
145
|
+
return wildCardValues.every((wildCardValue: string) =>
|
|
146
|
+
value.toLowerCase().includes(wildCardValue.toLowerCase()),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return wildCardValues.every((wildCardValue: string) =>
|
|
151
|
+
value.includes(wildCardValue),
|
|
152
|
+
);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
return generateFieldMatchFunc(
|
|
156
|
+
generateWildcardMatchFunc,
|
|
157
|
+
searchQuery.wildcard,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
throw new Error("Unsupported search query expression");
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const generateFieldMatchFunc = (
|
|
165
|
+
matchFunc: (value: any) => boolean,
|
|
166
|
+
searchQuery: _SearchQueryExpressionValue,
|
|
167
|
+
) => {
|
|
168
|
+
const generateMatchFunc = (
|
|
169
|
+
obj: ProductProjection,
|
|
170
|
+
markMatchingVariants: boolean,
|
|
171
|
+
) => {
|
|
172
|
+
if (searchQuery.field.startsWith("variants.")) {
|
|
173
|
+
const variantField = searchQuery.field.substring(
|
|
174
|
+
searchQuery.field.indexOf(".") + 1,
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const variants = getVariants(obj) as Writable<ProductVariant>[];
|
|
178
|
+
for (const variant of variants) {
|
|
179
|
+
const value = resolveFieldValue(variant, {
|
|
180
|
+
...searchQuery,
|
|
181
|
+
field: variantField,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
if (matchFunc(value)) {
|
|
185
|
+
if (markMatchingVariants) {
|
|
186
|
+
for (const v of variants) {
|
|
187
|
+
v.isMatchingVariant = false;
|
|
188
|
+
}
|
|
189
|
+
variant.isMatchingVariant = true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return matchFunc(resolveFieldValue(obj, searchQuery));
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
return generateMatchFunc;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const resolveFieldValue = (
|
|
206
|
+
obj: any,
|
|
207
|
+
searchQuery: _SearchQueryExpressionValue,
|
|
208
|
+
) => {
|
|
209
|
+
if (searchQuery.field === undefined) {
|
|
210
|
+
throw new Error("Missing field path in query expression");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let fieldPath = searchQuery.field;
|
|
214
|
+
const language = "language" in searchQuery ? searchQuery.language : undefined;
|
|
215
|
+
|
|
216
|
+
if (fieldPath.startsWith("variants.")) {
|
|
217
|
+
fieldPath = fieldPath.substring(fieldPath.indexOf(".") + 1);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (fieldPath.startsWith("attributes.")) {
|
|
221
|
+
const [, attrName, ...rest] = fieldPath.split(".");
|
|
222
|
+
if (!obj.attributes) {
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
for (const attr of obj.attributes) {
|
|
227
|
+
if (attr.name === attrName) {
|
|
228
|
+
return nestedLookupByLanguage(attr.value, rest.join("."), language);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (fieldPath === "prices.currentCentAmount") {
|
|
234
|
+
return obj.prices && obj.prices.length > 0
|
|
235
|
+
? obj.prices[0].value.centAmount
|
|
236
|
+
: undefined;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return nestedLookupByLanguage(obj, fieldPath, language);
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const nestedLookupByLanguage = (
|
|
243
|
+
obj: any,
|
|
244
|
+
path: string,
|
|
245
|
+
language?: string,
|
|
246
|
+
): any => {
|
|
247
|
+
const value = nestedLookup(obj, path);
|
|
248
|
+
|
|
249
|
+
if (language && value && typeof value === "object") {
|
|
250
|
+
// Due to Commercetools supporting "en", but also "en-US" as language, we need to find the best match
|
|
251
|
+
const matchingLanguageKey = Object.keys(value).find((key) =>
|
|
252
|
+
key.toLowerCase().startsWith(language.toLowerCase()),
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
return matchingLanguageKey ? value[matchingLanguageKey] : undefined;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return value;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// type guards
|
|
262
|
+
const isSearchAndExpression = (
|
|
263
|
+
expr: _SearchQuery,
|
|
264
|
+
): expr is SearchAndExpression =>
|
|
265
|
+
(expr as SearchAndExpression).and !== undefined;
|
|
266
|
+
|
|
267
|
+
const isSearchOrExpression = (expr: _SearchQuery): expr is SearchOrExpression =>
|
|
268
|
+
(expr as SearchOrExpression).or !== undefined;
|
|
269
|
+
|
|
270
|
+
type SearchRangeExpression =
|
|
271
|
+
| SearchDateRangeExpression
|
|
272
|
+
| SearchDateTimeRangeExpression
|
|
273
|
+
| SearchLongRangeExpression
|
|
274
|
+
| SearchNumberRangeExpression
|
|
275
|
+
| SearchTimeRangeExpression;
|
|
276
|
+
|
|
277
|
+
// Type guard for SearchNotExpression
|
|
278
|
+
const isSearchNotExpression = (
|
|
279
|
+
expr: _SearchQueryExpression,
|
|
280
|
+
): expr is SearchNotExpression =>
|
|
281
|
+
(expr as SearchNotExpression).not !== undefined;
|
|
282
|
+
|
|
283
|
+
// Type guard for SearchFilterExpression
|
|
284
|
+
const isSearchFilterExpression = (
|
|
285
|
+
expr: _SearchQueryExpression,
|
|
286
|
+
): expr is SearchFilterExpression =>
|
|
287
|
+
(expr as SearchFilterExpression).filter !== undefined;
|
|
288
|
+
|
|
289
|
+
// Type guard for SearchDateRangeExpression
|
|
290
|
+
const isSearchRangeExpression = (
|
|
291
|
+
expr: _SearchQueryExpression,
|
|
292
|
+
): expr is SearchRangeExpression =>
|
|
293
|
+
(expr as SearchRangeExpression).range !== undefined;
|
|
294
|
+
|
|
295
|
+
// Type guard for SearchExactExpression
|
|
296
|
+
const isSearchExactExpression = (
|
|
297
|
+
expr: _SearchQueryExpression,
|
|
298
|
+
): expr is SearchExactExpression =>
|
|
299
|
+
(expr as SearchExactExpression).exact !== undefined;
|
|
300
|
+
|
|
301
|
+
// Type guard for SearchExistsExpression
|
|
302
|
+
const isSearchExistsExpression = (
|
|
303
|
+
expr: _SearchQueryExpression,
|
|
304
|
+
): expr is SearchExistsExpression =>
|
|
305
|
+
(expr as SearchExistsExpression).exists !== undefined;
|
|
306
|
+
|
|
307
|
+
// Type guard for SearchFullTextExpression
|
|
308
|
+
const isSearchFullTextExpression = (
|
|
309
|
+
expr: _SearchQueryExpression,
|
|
310
|
+
): expr is SearchFullTextExpression =>
|
|
311
|
+
(expr as SearchFullTextExpression).fullText !== undefined;
|
|
312
|
+
|
|
313
|
+
// Type guard for SearchFullTextPrefixExpression
|
|
314
|
+
const isSearchFullTextPrefixExpression = (
|
|
315
|
+
expr: _SearchQueryExpression,
|
|
316
|
+
): expr is SearchFullTextPrefixExpression =>
|
|
317
|
+
(expr as SearchFullTextPrefixExpression).fullTextPrefix !== undefined;
|
|
318
|
+
|
|
319
|
+
// Type guard for SearchPrefixExpression
|
|
320
|
+
const isSearchPrefixExpression = (
|
|
321
|
+
expr: _SearchQueryExpression,
|
|
322
|
+
): expr is SearchPrefixExpression =>
|
|
323
|
+
(expr as SearchPrefixExpression).prefix !== undefined;
|
|
324
|
+
|
|
325
|
+
// Type guard for SearchWildCardExpression
|
|
326
|
+
const isSearchWildCardExpression = (
|
|
327
|
+
expr: _SearchQueryExpression,
|
|
328
|
+
): expr is SearchWildCardExpression =>
|
|
329
|
+
(expr as SearchWildCardExpression).wildcard !== undefined;
|
package/src/product-search.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
ProductSearchResult,
|
|
8
8
|
} from "@commercetools/platform-sdk";
|
|
9
9
|
import { CommercetoolsError } from "./exceptions";
|
|
10
|
+
import { parseSearchQuery } from "./lib/productSearchFilter";
|
|
10
11
|
import { validateSearchQuery } from "./lib/searchQueryTypeChecker";
|
|
11
12
|
import { applyPriceSelector } from "./priceSelector";
|
|
12
13
|
import { AbstractStorage } from "./storage";
|
|
@@ -22,7 +23,7 @@ export class ProductSearch {
|
|
|
22
23
|
projectKey: string,
|
|
23
24
|
params: ProductSearchRequest,
|
|
24
25
|
): ProductPagedSearchResponse {
|
|
25
|
-
|
|
26
|
+
let resources = this._storage
|
|
26
27
|
.all(projectKey, "product")
|
|
27
28
|
.map((r) =>
|
|
28
29
|
this.transform(r, params.productProjectionParameters?.staged ?? false),
|
|
@@ -34,10 +35,19 @@ export class ProductSearch {
|
|
|
34
35
|
return true;
|
|
35
36
|
});
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
const markMatchingVariant = params.markMatchingVariants ?? false;
|
|
39
|
+
|
|
40
|
+
// Apply filters pre facetting
|
|
38
41
|
if (params.query) {
|
|
39
42
|
try {
|
|
40
43
|
validateSearchQuery(params.query);
|
|
44
|
+
|
|
45
|
+
const matchFunc = parseSearchQuery(params.query);
|
|
46
|
+
|
|
47
|
+
// Filters can modify the output. So clone the resources first.
|
|
48
|
+
resources = resources.filter((resource) =>
|
|
49
|
+
matchFunc(resource, markMatchingVariant),
|
|
50
|
+
);
|
|
41
51
|
} catch (err) {
|
|
42
52
|
console.error(err);
|
|
43
53
|
throw new CommercetoolsError<InvalidInputError>(
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
OrderAddReturnInfoAction,
|
|
7
7
|
OrderChangeOrderStateAction,
|
|
8
8
|
OrderChangePaymentStateAction,
|
|
9
|
+
OrderChangeShipmentStateAction,
|
|
9
10
|
OrderSetBillingAddressAction,
|
|
10
11
|
OrderSetCustomFieldAction,
|
|
11
12
|
OrderSetCustomTypeAction,
|
|
@@ -117,6 +118,14 @@ export class OrderUpdateHandler
|
|
|
117
118
|
resource.paymentState = paymentState;
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
changeShipmentState(
|
|
122
|
+
context: RepositoryContext,
|
|
123
|
+
resource: Writable<Order>,
|
|
124
|
+
{ shipmentState }: OrderChangeShipmentStateAction,
|
|
125
|
+
) {
|
|
126
|
+
resource.shipmentState = shipmentState;
|
|
127
|
+
}
|
|
128
|
+
|
|
120
129
|
setBillingAddress(
|
|
121
130
|
context: RepositoryContext,
|
|
122
131
|
resource: Writable<Order>,
|