@labdigital/commercetools-mock 0.7.0 → 0.9.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.d.ts +410 -0
- package/dist/index.global.js +50059 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +4913 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +4879 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +14 -11
- package/src/helpers.ts +24 -0
- package/src/lib/projectionSearchFilter.test.ts +6 -0
- package/src/lib/projectionSearchFilter.ts +173 -74
- package/src/product-projection-search.ts +210 -10
- package/src/repositories/abstract.ts +23 -5
- package/src/repositories/extension.ts +18 -1
- package/src/repositories/helpers.ts +37 -3
- package/src/repositories/product-projection.ts +1 -0
- package/src/repositories/project.ts +10 -5
- package/src/repositories/state.ts +14 -0
- package/src/services/product-projection.test.ts +170 -3
- package/src/validate.js +0 -1
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.9.0",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"typings": "dist/index.d.ts",
|
|
@@ -7,16 +7,15 @@
|
|
|
7
7
|
"dist",
|
|
8
8
|
"src"
|
|
9
9
|
],
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"require": "./dist/index.js",
|
|
13
|
+
"import": "./dist/index.mjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
10
16
|
"engines": {
|
|
11
17
|
"node": ">=14"
|
|
12
18
|
},
|
|
13
|
-
"scripts": {
|
|
14
|
-
"start": "nodemon --watch src --exec 'node -r esbuild-register' src/server.ts",
|
|
15
|
-
"generate": "node -r esbuild-register generate-validation.ts",
|
|
16
|
-
"build": "tsup",
|
|
17
|
-
"test": "jest test --coverage",
|
|
18
|
-
"lint": "eslint"
|
|
19
|
-
},
|
|
20
19
|
"prettier": {
|
|
21
20
|
"printWidth": 80,
|
|
22
21
|
"semi": false,
|
|
@@ -57,8 +56,6 @@
|
|
|
57
56
|
},
|
|
58
57
|
"dependencies": {
|
|
59
58
|
"@types/lodash": "^4.14.182",
|
|
60
|
-
"ajv": "^8.8.2",
|
|
61
|
-
"ajv-formats": "^2.1.1",
|
|
62
59
|
"basic-auth": "^2.0.1",
|
|
63
60
|
"body-parser": "^1.20.0",
|
|
64
61
|
"deep-equal": "^2.0.5",
|
|
@@ -73,5 +70,11 @@
|
|
|
73
70
|
},
|
|
74
71
|
"peerDependencies": {
|
|
75
72
|
"@commercetools/platform-sdk": "^2.4.1"
|
|
73
|
+
},
|
|
74
|
+
"scripts": {
|
|
75
|
+
"start": "nodemon --watch src --exec 'node -r esbuild-register' src/server.ts",
|
|
76
|
+
"build": "tsup",
|
|
77
|
+
"test": "jest test --coverage",
|
|
78
|
+
"lint": "eslint"
|
|
76
79
|
}
|
|
77
|
-
}
|
|
80
|
+
}
|
package/src/helpers.ts
CHANGED
|
@@ -11,6 +11,30 @@ export const getBaseResourceProperties = () => {
|
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Do a nested lookup by using a path. For example `foo.bar.value` will
|
|
16
|
+
* return obj['foo']['bar']['value']
|
|
17
|
+
*/
|
|
18
|
+
export const nestedLookup = (obj: any, path: string): any => {
|
|
19
|
+
if (!path || path === '') {
|
|
20
|
+
return obj
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const parts = path.split('.')
|
|
24
|
+
let val = obj
|
|
25
|
+
|
|
26
|
+
for (let i = 0; i < parts.length; i++) {
|
|
27
|
+
const part = parts[i]
|
|
28
|
+
if (val == undefined) {
|
|
29
|
+
return undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
val = val[part]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return val
|
|
36
|
+
}
|
|
37
|
+
|
|
14
38
|
export const QueryParamsAsArray = (
|
|
15
39
|
input: string | ParsedQs | string[] | ParsedQs[] | undefined
|
|
16
40
|
): string[] => {
|
|
@@ -127,6 +127,12 @@ describe('Search filter', () => {
|
|
|
127
127
|
).toBeTruthy()
|
|
128
128
|
})
|
|
129
129
|
|
|
130
|
+
test('by price range - or', async () => {
|
|
131
|
+
expect(
|
|
132
|
+
match(`variants.price.centAmount:range (2 TO 1500 ), (1500 TO 3000), (3000 TO 6000)`).isMatch
|
|
133
|
+
).toBeTruthy()
|
|
134
|
+
})
|
|
135
|
+
|
|
130
136
|
test('by scopedPrice range', async () => {
|
|
131
137
|
let result
|
|
132
138
|
let products: Product[]
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { Product, ProductVariant } from '@commercetools/platform-sdk'
|
|
6
6
|
import perplex from 'perplex'
|
|
7
7
|
import Parser from 'pratt'
|
|
8
|
+
import { nestedLookup } from '../helpers'
|
|
8
9
|
import { Writable } from '../types'
|
|
9
10
|
|
|
10
11
|
type MatchFunc = (target: any) => boolean
|
|
@@ -14,6 +15,43 @@ type ProductFilter = (
|
|
|
14
15
|
markMatchingVariants: boolean
|
|
15
16
|
) => boolean
|
|
16
17
|
|
|
18
|
+
type Symbol = {
|
|
19
|
+
type: 'Symbol'
|
|
20
|
+
kind: 'int' | 'string' | 'any'
|
|
21
|
+
value: any
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type RangeExpressionSet = {
|
|
25
|
+
source: string
|
|
26
|
+
type: 'RangeExpression'
|
|
27
|
+
children?: RangeExpression[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type FilterExpressionSet = {
|
|
31
|
+
source: string
|
|
32
|
+
type: 'FilterExpression'
|
|
33
|
+
children?: FilterExpression[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type TermExpressionSet = {
|
|
37
|
+
source: string
|
|
38
|
+
type: 'TermExpression'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type ExpressionSet = RangeExpressionSet | FilterExpressionSet | TermExpressionSet
|
|
42
|
+
|
|
43
|
+
export type RangeExpression = {
|
|
44
|
+
type: 'RangeExpression'
|
|
45
|
+
start?: number
|
|
46
|
+
stop?: number
|
|
47
|
+
match: (obj: any) => boolean
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type FilterExpression = {
|
|
51
|
+
type: 'FilterExpression'
|
|
52
|
+
match: (obj: any) => boolean
|
|
53
|
+
}
|
|
54
|
+
|
|
17
55
|
/**
|
|
18
56
|
* Returns a function (ProductFilter).
|
|
19
57
|
* NOTE: The filter can alter the resources in-place (FIXME)
|
|
@@ -53,7 +91,7 @@ const getLexer = (value: string) => {
|
|
|
53
91
|
.token('WS', /\s+/, true) // skip
|
|
54
92
|
}
|
|
55
93
|
|
|
56
|
-
const
|
|
94
|
+
const parseFilter = (filter: string): ExpressionSet => {
|
|
57
95
|
const lexer = getLexer(filter)
|
|
58
96
|
const parser = new Parser(lexer)
|
|
59
97
|
.builder()
|
|
@@ -61,42 +99,81 @@ const generateMatchFunc = (filter: string): MatchFunc => {
|
|
|
61
99
|
return t.token.match
|
|
62
100
|
})
|
|
63
101
|
.led(':', 100, ({ left, bp }) => {
|
|
64
|
-
|
|
102
|
+
let parsed: any = parser.parse({ terminals: [bp - 1] })
|
|
103
|
+
let expressions: RangeExpression[] | FilterExpression[] | Symbol[]
|
|
104
|
+
expressions = !Array.isArray(parsed) ? [parsed] : parsed
|
|
65
105
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
106
|
+
// Make sure we only have one type of expression (cannot mix)
|
|
107
|
+
const unique = new Set(expressions.map(expr => expr.type))
|
|
108
|
+
if (unique.size > 1) {
|
|
109
|
+
throw new Error('Invalid expression')
|
|
70
110
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
111
|
+
|
|
112
|
+
// Convert plain symbols to a filter expression. For example
|
|
113
|
+
// variants.attribute.foobar:4 where 4 is a Symbol should result
|
|
114
|
+
// in a comparison
|
|
115
|
+
if (expressions.some(expr => expr.type == 'Symbol')) {
|
|
116
|
+
return {
|
|
117
|
+
source: left as string,
|
|
118
|
+
type: 'FilterExpression',
|
|
119
|
+
children: expressions.map((e): FilterExpression => {
|
|
120
|
+
if (e.type != 'Symbol') {
|
|
121
|
+
throw new Error('Invalid expression')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
type: 'FilterExpression',
|
|
126
|
+
match: (obj: any): boolean => {
|
|
127
|
+
return obj === e.value
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
})
|
|
74
131
|
}
|
|
75
132
|
}
|
|
76
|
-
|
|
77
|
-
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
source: left,
|
|
136
|
+
type: expressions[0].type,
|
|
137
|
+
children: expressions,
|
|
78
138
|
}
|
|
79
139
|
})
|
|
80
140
|
.nud('STRING', 20, t => {
|
|
81
|
-
|
|
82
|
-
|
|
141
|
+
return {
|
|
142
|
+
type: 'Symbol',
|
|
143
|
+
kind: 'string',
|
|
144
|
+
// @ts-ignore
|
|
145
|
+
value: t.token.groups[1],
|
|
146
|
+
} as Symbol
|
|
83
147
|
})
|
|
84
148
|
.nud('INT', 5, t => {
|
|
85
|
-
|
|
86
|
-
|
|
149
|
+
return {
|
|
150
|
+
type: 'Symbol',
|
|
151
|
+
kind: 'int',
|
|
152
|
+
value: parseInt(t.token.match, 10),
|
|
153
|
+
} as Symbol
|
|
87
154
|
})
|
|
88
155
|
.nud('STAR', 5, t => {
|
|
89
|
-
return
|
|
156
|
+
return {
|
|
157
|
+
type: 'Symbol',
|
|
158
|
+
kind: 'any',
|
|
159
|
+
value: null,
|
|
160
|
+
}
|
|
90
161
|
})
|
|
91
162
|
.nud('EXISTS', 10, ({ bp }) => {
|
|
92
|
-
return
|
|
93
|
-
|
|
94
|
-
|
|
163
|
+
return {
|
|
164
|
+
type: 'FilterExpression',
|
|
165
|
+
match: (obj: any): boolean => {
|
|
166
|
+
return obj !== undefined
|
|
167
|
+
},
|
|
168
|
+
} as FilterExpression
|
|
95
169
|
})
|
|
96
170
|
.nud('MISSING', 10, ({ bp }) => {
|
|
97
|
-
return
|
|
98
|
-
|
|
99
|
-
|
|
171
|
+
return {
|
|
172
|
+
type: 'FilterExpression',
|
|
173
|
+
match: (obj: any): boolean => {
|
|
174
|
+
return obj === undefined
|
|
175
|
+
},
|
|
176
|
+
} as FilterExpression
|
|
100
177
|
})
|
|
101
178
|
.led('COMMA', 200, ({ left, token, bp }) => {
|
|
102
179
|
const expr: any = parser.parse({ terminals: [bp - 1] })
|
|
@@ -106,52 +183,92 @@ const generateMatchFunc = (filter: string): MatchFunc => {
|
|
|
106
183
|
return [left, expr]
|
|
107
184
|
}
|
|
108
185
|
})
|
|
186
|
+
.nud('(', 100, t => {
|
|
187
|
+
const expr: any = parser.parse({ terminals: [')'] })
|
|
188
|
+
lexer.expect(')')
|
|
189
|
+
return expr
|
|
190
|
+
})
|
|
109
191
|
.bp(')', 0)
|
|
110
192
|
.led('TO', 20, ({ left, bp }) => {
|
|
111
193
|
const expr: any = parser.parse({ terminals: [bp - 1] })
|
|
112
|
-
return
|
|
194
|
+
return {
|
|
195
|
+
start: left.value,
|
|
196
|
+
stop: expr.value,
|
|
197
|
+
}
|
|
113
198
|
})
|
|
114
199
|
.nud('RANGE', 20, ({ bp }) => {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
else if (start === null && stop !== null) {
|
|
124
|
-
return (obj: any): boolean => {
|
|
125
|
-
return obj <= stop
|
|
126
|
-
}
|
|
200
|
+
let ranges: any = parser.parse()
|
|
201
|
+
|
|
202
|
+
// If multiple ranges are defined we receive an array of ranges. So let's
|
|
203
|
+
// make sure we always have an array
|
|
204
|
+
if (!Array.isArray(ranges)) {
|
|
205
|
+
ranges = [ranges]
|
|
127
206
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
207
|
+
|
|
208
|
+
// Return a list of functions which matches the ranges. These functions
|
|
209
|
+
// are processed as an OR clause
|
|
210
|
+
return ranges.map((range: any) => {
|
|
211
|
+
let func = undefined
|
|
212
|
+
|
|
213
|
+
if (range.start !== null && range.stop !== null) {
|
|
214
|
+
func = (obj: any): boolean => {
|
|
215
|
+
return obj >= range.start && obj <= range.stop
|
|
216
|
+
}
|
|
217
|
+
} else if (range.start === null && range.stop !== null) {
|
|
218
|
+
func = (obj: any): boolean => {
|
|
219
|
+
return obj <= range.stop
|
|
220
|
+
}
|
|
221
|
+
} else if (range.start !== null && range.stop === null) {
|
|
222
|
+
func = (obj: any): boolean => {
|
|
223
|
+
return obj >= range.start
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
func = (obj: any): boolean => {
|
|
227
|
+
return true
|
|
228
|
+
}
|
|
135
229
|
}
|
|
136
230
|
|
|
137
|
-
|
|
231
|
+
return {
|
|
232
|
+
type: 'RangeExpression',
|
|
233
|
+
start: range.start,
|
|
234
|
+
stop: range.stop,
|
|
235
|
+
match: func,
|
|
236
|
+
} as RangeExpression
|
|
237
|
+
})
|
|
138
238
|
})
|
|
139
239
|
.build()
|
|
140
240
|
|
|
141
|
-
|
|
241
|
+
return parser.parse()
|
|
242
|
+
}
|
|
142
243
|
|
|
143
|
-
|
|
244
|
+
const generateMatchFunc = (filter: string) => {
|
|
245
|
+
const result = parseFilter(filter)
|
|
246
|
+
if (!result) {
|
|
144
247
|
const lines = filter.split('\n')
|
|
145
248
|
const column = lines[lines.length - 1].length
|
|
146
249
|
throw new Error(`Syntax error while parsing '${filter}'.`)
|
|
147
250
|
}
|
|
148
|
-
|
|
251
|
+
if (result.type == 'TermExpression') {
|
|
252
|
+
throw new Error(`Syntax error while parsing '${filter}'.`)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return (obj: any) => {
|
|
256
|
+
if (!result.children) return false
|
|
257
|
+
return result.children.some(c => c.match(obj))
|
|
258
|
+
}
|
|
149
259
|
}
|
|
150
260
|
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
261
|
+
export const generateFacetFunc = (filter: string): ExpressionSet => {
|
|
262
|
+
if (!filter.includes(':')) {
|
|
263
|
+
return {
|
|
264
|
+
source: filter,
|
|
265
|
+
type: 'TermExpression',
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return parseFilter(filter)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const filterProduct = (source: string, exprFunc: MatchFunc): ProductFilter => {
|
|
155
272
|
return (p: Product, markMatchingVariants: boolean): boolean => {
|
|
156
273
|
const value = nestedLookup(p, source)
|
|
157
274
|
return exprFunc(value)
|
|
@@ -172,13 +289,12 @@ const filterVariants = (
|
|
|
172
289
|
const value = resolveVariantValue(variant, path)
|
|
173
290
|
|
|
174
291
|
if (exprFunc(value)) {
|
|
175
|
-
|
|
176
292
|
// If markMatchingVariants parameter is true those ProductVariants that
|
|
177
293
|
// match the search query have the additional field isMatchingVariant
|
|
178
294
|
// set to true. For the other variants in the same product projection
|
|
179
295
|
// this field is set to false.
|
|
180
296
|
if (markMatchingVariants) {
|
|
181
|
-
variants.forEach(v => v.isMatchingVariant = false)
|
|
297
|
+
variants.forEach(v => (v.isMatchingVariant = false))
|
|
182
298
|
variant.isMatchingVariant = true
|
|
183
299
|
}
|
|
184
300
|
return true
|
|
@@ -189,10 +305,13 @@ const filterVariants = (
|
|
|
189
305
|
}
|
|
190
306
|
}
|
|
191
307
|
|
|
192
|
-
const resolveVariantValue = (obj: ProductVariant, path: string): any => {
|
|
308
|
+
export const resolveVariantValue = (obj: ProductVariant, path: string): any => {
|
|
193
309
|
if (path === undefined) {
|
|
194
310
|
return obj
|
|
195
311
|
}
|
|
312
|
+
if (path.startsWith('variants.')) {
|
|
313
|
+
path = path.substring(path.indexOf('.') + 1)
|
|
314
|
+
}
|
|
196
315
|
|
|
197
316
|
if (path.startsWith('attributes.')) {
|
|
198
317
|
const [, attrName, ...rest] = path.split('.')
|
|
@@ -216,27 +335,7 @@ const resolveVariantValue = (obj: ProductVariant, path: string): any => {
|
|
|
216
335
|
return nestedLookup(obj, path)
|
|
217
336
|
}
|
|
218
337
|
|
|
219
|
-
const
|
|
220
|
-
if (!path || path === '') {
|
|
221
|
-
return obj
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const parts = path.split('.')
|
|
225
|
-
let val = obj
|
|
226
|
-
|
|
227
|
-
for (let i = 0; i < parts.length; i++) {
|
|
228
|
-
const part = parts[i]
|
|
229
|
-
if (val == undefined) {
|
|
230
|
-
return undefined
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
val = val[part]
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return val
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const getVariants = (p: Product, staged: boolean): ProductVariant[] => {
|
|
338
|
+
export const getVariants = (p: Product, staged: boolean): ProductVariant[] => {
|
|
240
339
|
return [
|
|
241
340
|
staged
|
|
242
341
|
? p.masterData.staged?.masterVariant
|
|
@@ -4,9 +4,25 @@ import {
|
|
|
4
4
|
Product,
|
|
5
5
|
ProductProjection,
|
|
6
6
|
QueryParam,
|
|
7
|
+
FacetResults,
|
|
8
|
+
FacetTerm,
|
|
9
|
+
TermFacetResult,
|
|
10
|
+
RangeFacetResult,
|
|
11
|
+
FilteredFacetResult,
|
|
7
12
|
} from '@commercetools/platform-sdk'
|
|
13
|
+
import { ByProjectKeyProductProjectionsSearchRequestBuilder } from '@commercetools/platform-sdk/dist/declarations/src/generated/client/search/by-project-key-product-projections-search-request-builder'
|
|
14
|
+
import { nestedLookup } from './helpers'
|
|
15
|
+
import { ProductService } from './services/product'
|
|
16
|
+
import { Writable } from './types'
|
|
8
17
|
import { CommercetoolsError } from './exceptions'
|
|
9
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
FilterExpression,
|
|
20
|
+
generateFacetFunc,
|
|
21
|
+
getVariants,
|
|
22
|
+
parseFilterExpression,
|
|
23
|
+
RangeExpression,
|
|
24
|
+
resolveVariantValue,
|
|
25
|
+
} from './lib/projectionSearchFilter'
|
|
10
26
|
import { applyPriceSelector } from './priceSelector'
|
|
11
27
|
import { AbstractStorage } from './storage'
|
|
12
28
|
|
|
@@ -68,10 +84,9 @@ export class ProductProjectionSearch {
|
|
|
68
84
|
)
|
|
69
85
|
|
|
70
86
|
// Filters can modify the output. So clone the resources first.
|
|
71
|
-
resources = resources
|
|
72
|
-
.
|
|
73
|
-
|
|
74
|
-
)
|
|
87
|
+
resources = resources.filter(resource =>
|
|
88
|
+
filters.every(f => f(resource, markMatchingVariant))
|
|
89
|
+
)
|
|
75
90
|
} catch (err) {
|
|
76
91
|
throw new CommercetoolsError<InvalidInputError>(
|
|
77
92
|
{
|
|
@@ -84,6 +99,7 @@ export class ProductProjectionSearch {
|
|
|
84
99
|
}
|
|
85
100
|
|
|
86
101
|
// TODO: Calculate facets
|
|
102
|
+
const facets = this.getFacets(params, resources)
|
|
87
103
|
|
|
88
104
|
// Apply filters post facetting
|
|
89
105
|
if (params['filter.query']) {
|
|
@@ -91,10 +107,10 @@ export class ProductProjectionSearch {
|
|
|
91
107
|
const filters = params['filter.query'].map(f =>
|
|
92
108
|
parseFilterExpression(f, params.staged ?? false)
|
|
93
109
|
)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
110
|
+
|
|
111
|
+
resources = resources.filter(resource =>
|
|
112
|
+
filters.every(f => f(resource, markMatchingVariant))
|
|
113
|
+
)
|
|
98
114
|
} catch (err) {
|
|
99
115
|
throw new CommercetoolsError<InvalidInputError>(
|
|
100
116
|
{
|
|
@@ -127,7 +143,7 @@ export class ProductProjectionSearch {
|
|
|
127
143
|
offset: offset,
|
|
128
144
|
limit: limit,
|
|
129
145
|
results: resources.map(this.transform),
|
|
130
|
-
facets:
|
|
146
|
+
facets: facets,
|
|
131
147
|
}
|
|
132
148
|
}
|
|
133
149
|
|
|
@@ -139,6 +155,9 @@ export class ProductProjectionSearch {
|
|
|
139
155
|
lastModifiedAt: product.lastModifiedAt,
|
|
140
156
|
version: product.version,
|
|
141
157
|
name: obj.name,
|
|
158
|
+
key: product.key,
|
|
159
|
+
description: obj.description,
|
|
160
|
+
metaDescription: obj.metaDescription,
|
|
142
161
|
slug: obj.slug,
|
|
143
162
|
categories: obj.categories,
|
|
144
163
|
masterVariant: obj.masterVariant,
|
|
@@ -146,4 +165,185 @@ export class ProductProjectionSearch {
|
|
|
146
165
|
productType: product.productType,
|
|
147
166
|
}
|
|
148
167
|
}
|
|
168
|
+
|
|
169
|
+
getFacets(
|
|
170
|
+
params: ProductProjectionSearchParams,
|
|
171
|
+
products: Product[]
|
|
172
|
+
): FacetResults {
|
|
173
|
+
if (!params.facet) return {}
|
|
174
|
+
const staged = false
|
|
175
|
+
const result: FacetResults = {}
|
|
176
|
+
|
|
177
|
+
for (const facet of params.facet) {
|
|
178
|
+
const expression = generateFacetFunc(facet)
|
|
179
|
+
|
|
180
|
+
// Term Facet
|
|
181
|
+
if (expression.type === 'TermExpression') {
|
|
182
|
+
result[facet] = this.termFacet(expression.source, products, staged)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Range Facet
|
|
186
|
+
if (expression.type === 'RangeExpression') {
|
|
187
|
+
result[expression.source] = this.rangeFacet(
|
|
188
|
+
expression.source,
|
|
189
|
+
expression.children,
|
|
190
|
+
products,
|
|
191
|
+
staged
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// FilteredFacet
|
|
196
|
+
if (expression.type === 'FilterExpression') {
|
|
197
|
+
result[expression.source] = this.filterFacet(
|
|
198
|
+
expression.source,
|
|
199
|
+
expression.children,
|
|
200
|
+
products,
|
|
201
|
+
staged
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return result
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* TODO: This implemention needs the following additional features:
|
|
211
|
+
* - counting products
|
|
212
|
+
* - correct dataType
|
|
213
|
+
*/
|
|
214
|
+
termFacet(
|
|
215
|
+
facet: string,
|
|
216
|
+
products: Product[],
|
|
217
|
+
staged: boolean
|
|
218
|
+
): TermFacetResult {
|
|
219
|
+
const result: Writable<TermFacetResult> = {
|
|
220
|
+
type: 'terms',
|
|
221
|
+
dataType: 'text',
|
|
222
|
+
missing: 0,
|
|
223
|
+
total: 0,
|
|
224
|
+
other: 0,
|
|
225
|
+
terms: [],
|
|
226
|
+
}
|
|
227
|
+
const terms: Record<any, number> = {}
|
|
228
|
+
|
|
229
|
+
if (facet.startsWith('variants.')) {
|
|
230
|
+
products.forEach(p => {
|
|
231
|
+
const variants = getVariants(p, staged)
|
|
232
|
+
variants.forEach(v => {
|
|
233
|
+
result.total++
|
|
234
|
+
|
|
235
|
+
let value = resolveVariantValue(v, facet)
|
|
236
|
+
if (value === undefined) {
|
|
237
|
+
result.missing++
|
|
238
|
+
} else {
|
|
239
|
+
if (typeof value === 'number') {
|
|
240
|
+
value = Number(value).toFixed(1)
|
|
241
|
+
}
|
|
242
|
+
terms[value] = value in terms ? terms[value] + 1 : 1
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
})
|
|
246
|
+
} else {
|
|
247
|
+
products.forEach(p => {
|
|
248
|
+
const value = nestedLookup(p, facet)
|
|
249
|
+
result.total++
|
|
250
|
+
if (value === undefined) {
|
|
251
|
+
result.missing++
|
|
252
|
+
} else {
|
|
253
|
+
terms[value] = value in terms ? terms[value] + 1 : 1
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
for (const term in terms) {
|
|
258
|
+
result.terms.push({
|
|
259
|
+
term: term as any,
|
|
260
|
+
count: terms[term],
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
return result
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
filterFacet(
|
|
267
|
+
source: string,
|
|
268
|
+
filters: FilterExpression[] | undefined,
|
|
269
|
+
products: Product[],
|
|
270
|
+
staged: boolean
|
|
271
|
+
): FilteredFacetResult {
|
|
272
|
+
let count = 0
|
|
273
|
+
if (source.startsWith('variants.')) {
|
|
274
|
+
for (const p of products) {
|
|
275
|
+
for (const v of getVariants(p, staged)) {
|
|
276
|
+
const val = resolveVariantValue(v, source)
|
|
277
|
+
if (filters?.some(f => f.match(val))) {
|
|
278
|
+
count++
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
throw new Error('not supported')
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
type: 'filter',
|
|
288
|
+
count: count,
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
rangeFacet(
|
|
293
|
+
source: string,
|
|
294
|
+
ranges: RangeExpression[] | undefined,
|
|
295
|
+
products: Product[],
|
|
296
|
+
staged: boolean
|
|
297
|
+
): RangeFacetResult {
|
|
298
|
+
const counts =
|
|
299
|
+
ranges?.map(range => {
|
|
300
|
+
if (source.startsWith('variants.')) {
|
|
301
|
+
const values = []
|
|
302
|
+
for (const p of products) {
|
|
303
|
+
for (const v of getVariants(p, staged)) {
|
|
304
|
+
const val = resolveVariantValue(v, source)
|
|
305
|
+
if (val === undefined) {
|
|
306
|
+
continue
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (range.match(val)) {
|
|
310
|
+
values.push(val)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const numValues = values.length
|
|
316
|
+
return {
|
|
317
|
+
type: 'double',
|
|
318
|
+
from: range.start || 0,
|
|
319
|
+
fromStr: range.start !== null ? Number(range.start).toFixed(1) : '',
|
|
320
|
+
to: range.stop || 0,
|
|
321
|
+
toStr: range.stop !== null ? Number(range.stop).toFixed(1) : '',
|
|
322
|
+
count: numValues,
|
|
323
|
+
// totalCount: 0,
|
|
324
|
+
total: values.reduce((a, b) => a + b, 0),
|
|
325
|
+
min: numValues > 0 ? Math.min(...values) : 0,
|
|
326
|
+
max: numValues > 0 ? Math.max(...values) : 0,
|
|
327
|
+
mean: numValues > 0 ? mean(values) : 0,
|
|
328
|
+
}
|
|
329
|
+
} else {
|
|
330
|
+
throw new Error('not supported')
|
|
331
|
+
}
|
|
332
|
+
}) || []
|
|
333
|
+
const data: RangeFacetResult = {
|
|
334
|
+
type: 'range',
|
|
335
|
+
// @ts-ignore
|
|
336
|
+
dataType: 'number',
|
|
337
|
+
ranges: counts,
|
|
338
|
+
}
|
|
339
|
+
return data
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const mean = (arr: number[]) => {
|
|
344
|
+
let total = 0
|
|
345
|
+
for (let i = 0; i < arr.length; i++) {
|
|
346
|
+
total += arr[i]
|
|
347
|
+
}
|
|
348
|
+
return total / arr.length
|
|
149
349
|
}
|