@labdigital/commercetools-mock 0.6.3 → 0.7.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/package.json +26 -13
- package/src/ctMock.ts +5 -0
- package/src/helpers.ts +15 -0
- package/src/lib/projectionSearchFilter.test.ts +177 -0
- package/src/lib/projectionSearchFilter.ts +248 -0
- package/src/priceSelector.test.ts +96 -0
- package/src/priceSelector.ts +109 -0
- package/src/product-projection-search.ts +149 -0
- package/src/projectAPI.ts +20 -21
- package/src/repositories/category.ts +36 -0
- package/src/repositories/channel.ts +104 -0
- package/src/repositories/customer-group.ts +37 -0
- package/src/repositories/discount-code.ts +37 -0
- package/src/repositories/errors.ts +3 -3
- package/src/repositories/helpers.ts +46 -4
- package/src/repositories/product-discount.ts +181 -0
- package/src/repositories/product-projection.ts +29 -55
- package/src/repositories/product-type.ts +88 -6
- package/src/repositories/product.ts +49 -9
- package/src/repositories/shipping-method.ts +31 -0
- package/src/repositories/store.ts +43 -3
- package/src/repositories/type.ts +19 -0
- package/src/services/abstract.ts +1 -4
- package/src/services/custom-object.test.ts +2 -2
- package/src/services/product-discount.ts +33 -0
- package/src/services/product-projection.test.ts +171 -116
- package/src/services/product.test.ts +12 -0
- package/src/services/project.ts +0 -1
- package/src/storage.ts +117 -59
- package/src/types.ts +9 -2
- package/dist/commercetools-mock.cjs.development.js +0 -4380
- package/dist/commercetools-mock.cjs.development.js.map +0 -1
- package/dist/commercetools-mock.cjs.production.min.js +0 -2
- package/dist/commercetools-mock.cjs.production.min.js.map +0 -1
- package/dist/commercetools-mock.esm.js +0 -4372
- package/dist/commercetools-mock.esm.js.map +0 -1
- package/dist/constants.d.ts +0 -2
- package/dist/ctMock.d.ts +0 -32
- package/dist/exceptions.d.ts +0 -12
- package/dist/helpers.d.ts +0 -6
- package/dist/index.d.ts +0 -3
- package/dist/index.js +0 -8
- package/dist/lib/expandParser.d.ts +0 -15
- package/dist/lib/filterParser.d.ts +0 -1
- package/dist/lib/haversine.d.ts +0 -8
- package/dist/lib/masking.d.ts +0 -1
- package/dist/lib/predicateParser.d.ts +0 -11
- package/dist/lib/proxy.d.ts +0 -1
- package/dist/oauth/errors.d.ts +0 -8
- package/dist/oauth/helpers.d.ts +0 -2
- package/dist/oauth/server.d.ts +0 -12
- package/dist/oauth/store.d.ts +0 -14
- package/dist/projectAPI.d.ts +0 -12
- package/dist/repositories/abstract.d.ts +0 -33
- package/dist/repositories/cart-discount.d.ts +0 -9
- package/dist/repositories/cart.d.ts +0 -21
- package/dist/repositories/category.d.ts +0 -18
- package/dist/repositories/channel.d.ts +0 -6
- package/dist/repositories/custom-object.d.ts +0 -8
- package/dist/repositories/customer-group.d.ts +0 -11
- package/dist/repositories/customer.d.ts +0 -11
- package/dist/repositories/discount-code.d.ts +0 -8
- package/dist/repositories/errors.d.ts +0 -2
- package/dist/repositories/extension.d.ts +0 -8
- package/dist/repositories/helpers.d.ts +0 -10
- package/dist/repositories/inventory-entry.d.ts +0 -14
- package/dist/repositories/my-order.d.ts +0 -6
- package/dist/repositories/order.d.ts +0 -26
- package/dist/repositories/payment.d.ts +0 -23
- package/dist/repositories/product-projection.d.ts +0 -10
- package/dist/repositories/product-type.d.ts +0 -10
- package/dist/repositories/product.d.ts +0 -11
- package/dist/repositories/project.d.ts +0 -8
- package/dist/repositories/shipping-method.d.ts +0 -10
- package/dist/repositories/shopping-list.d.ts +0 -6
- package/dist/repositories/state.d.ts +0 -8
- package/dist/repositories/store.d.ts +0 -10
- package/dist/repositories/subscription.d.ts +0 -6
- package/dist/repositories/tax-category.d.ts +0 -10
- package/dist/repositories/type.d.ts +0 -8
- package/dist/repositories/zone.d.ts +0 -8
- package/dist/server.d.ts +0 -1
- package/dist/services/abstract.d.ts +0 -20
- package/dist/services/cart-discount.d.ts +0 -9
- package/dist/services/cart.d.ts +0 -12
- package/dist/services/category.d.ts +0 -9
- package/dist/services/channel.d.ts +0 -9
- package/dist/services/custom-object.d.ts +0 -13
- package/dist/services/customer-group.d.ts +0 -9
- package/dist/services/customer.d.ts +0 -10
- package/dist/services/discount-code.d.ts +0 -9
- package/dist/services/extension.d.ts +0 -9
- package/dist/services/inventory-entry.d.ts +0 -9
- package/dist/services/my-cart.d.ts +0 -11
- package/dist/services/my-customer.d.ts +0 -13
- package/dist/services/my-order.d.ts +0 -10
- package/dist/services/my-payment.d.ts +0 -9
- package/dist/services/order.d.ts +0 -12
- package/dist/services/payment.d.ts +0 -9
- package/dist/services/product-projection.d.ts +0 -11
- package/dist/services/product-type.d.ts +0 -11
- package/dist/services/product.d.ts +0 -9
- package/dist/services/project.d.ts +0 -11
- package/dist/services/shipping-method.d.ts +0 -10
- package/dist/services/shopping-list.d.ts +0 -9
- package/dist/services/state.d.ts +0 -9
- package/dist/services/store.d.ts +0 -11
- package/dist/services/subscription.d.ts +0 -9
- package/dist/services/tax-category.d.ts +0 -11
- package/dist/services/type.d.ts +0 -9
- package/dist/services/zone.d.ts +0 -9
- package/dist/storage.d.ts +0 -56
- package/dist/types.d.ts +0 -89
- package/dist/validate.d.ts +0 -7482
- package/src/lib/filterParser.test.ts +0 -15
- package/src/lib/filterParser.ts +0 -17
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.7.0",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"typings": "dist/index.d.ts",
|
|
@@ -8,16 +8,14 @@
|
|
|
8
8
|
"src"
|
|
9
9
|
],
|
|
10
10
|
"engines": {
|
|
11
|
-
"node": ">=
|
|
11
|
+
"node": ">=14"
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
|
-
"start": "
|
|
15
|
-
"server": "nodemon --watch src --exec 'node -r esbuild-register' src/server.ts",
|
|
14
|
+
"start": "nodemon --watch src --exec 'node -r esbuild-register' src/server.ts",
|
|
16
15
|
"generate": "node -r esbuild-register generate-validation.ts",
|
|
17
|
-
"build": "
|
|
18
|
-
"test": "
|
|
19
|
-
"lint": "
|
|
20
|
-
"prepare": "tsdx build"
|
|
16
|
+
"build": "tsup",
|
|
17
|
+
"test": "jest test --coverage",
|
|
18
|
+
"lint": "eslint"
|
|
21
19
|
},
|
|
22
20
|
"prettier": {
|
|
23
21
|
"printWidth": 80,
|
|
@@ -29,34 +27,49 @@
|
|
|
29
27
|
"author": "Michael van Tellingen",
|
|
30
28
|
"module": "dist/commercetools--mock.esm.js",
|
|
31
29
|
"devDependencies": {
|
|
32
|
-
"@babel/preset-
|
|
33
|
-
"@
|
|
30
|
+
"@babel/preset-env": "^7.18.9",
|
|
31
|
+
"@babel/preset-typescript": "^7.18.6",
|
|
32
|
+
"@commercetools/platform-sdk": "2.8.0",
|
|
34
33
|
"@types/basic-auth": "^1.1.3",
|
|
34
|
+
"@types/body-parser": "^1.19.2",
|
|
35
35
|
"@types/deep-equal": "^1.0.1",
|
|
36
36
|
"@types/express": "^4.17.13",
|
|
37
|
+
"@types/express-serve-static-core": "^4.17.29",
|
|
38
|
+
"@types/jest": "^28.1.6",
|
|
37
39
|
"@types/morgan": "^1.9.3",
|
|
40
|
+
"@types/node": "*",
|
|
41
|
+
"@types/qs": "^6.9.7",
|
|
38
42
|
"@types/supertest": "^2.0.11",
|
|
39
43
|
"@types/uuid": "^8.3.4",
|
|
44
|
+
"esbuild": "^0.14.50",
|
|
40
45
|
"esbuild-register": "^3.3.1",
|
|
46
|
+
"eslint": "^8.20.0",
|
|
41
47
|
"got": "^11.8.3",
|
|
42
48
|
"husky": "^7.0.4",
|
|
49
|
+
"jest": "^28.1.3",
|
|
43
50
|
"nodemon": "^2.0.15",
|
|
51
|
+
"timekeeper": "^2.2.0",
|
|
44
52
|
"ts-node": "^10.4.0",
|
|
45
|
-
"
|
|
53
|
+
"tsc": "^2.0.4",
|
|
46
54
|
"tslib": "^2.3.1",
|
|
47
|
-
"
|
|
55
|
+
"tsup": "^6.2.0",
|
|
56
|
+
"typescript": "^4.7.4"
|
|
48
57
|
},
|
|
49
58
|
"dependencies": {
|
|
59
|
+
"@types/lodash": "^4.14.182",
|
|
50
60
|
"ajv": "^8.8.2",
|
|
51
61
|
"ajv-formats": "^2.1.1",
|
|
52
62
|
"basic-auth": "^2.0.1",
|
|
63
|
+
"body-parser": "^1.20.0",
|
|
53
64
|
"deep-equal": "^2.0.5",
|
|
54
65
|
"express": "^4.17.2",
|
|
66
|
+
"lodash": "^4.17.21",
|
|
55
67
|
"morgan": "^1.10.0",
|
|
56
68
|
"nock": "^13.2.1",
|
|
57
69
|
"perplex": "^0.11.0",
|
|
58
70
|
"pratt": "^0.7.0",
|
|
59
|
-
"supertest": "^6.1.6"
|
|
71
|
+
"supertest": "^6.1.6",
|
|
72
|
+
"uuid": "^8.3.2"
|
|
60
73
|
},
|
|
61
74
|
"peerDependencies": {
|
|
62
75
|
"@commercetools/platform-sdk": "^2.4.1"
|
package/src/ctMock.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { MyCartService } from './services/my-cart'
|
|
|
25
25
|
import { MyPaymentService } from './services/my-payment'
|
|
26
26
|
import { OrderService } from './services/order'
|
|
27
27
|
import { PaymentService } from './services/payment'
|
|
28
|
+
import { ProductDiscountService } from './services/product-discount'
|
|
28
29
|
import { ProductProjectionService } from './services/product-projection'
|
|
29
30
|
import { ProductService } from './services/product'
|
|
30
31
|
import { ProductTypeService } from './services/product-type'
|
|
@@ -180,6 +181,10 @@ export class CommercetoolsMock {
|
|
|
180
181
|
),
|
|
181
182
|
'product-type': new ProductTypeService(projectRouter, this._storage),
|
|
182
183
|
product: new ProductService(projectRouter, this._storage),
|
|
184
|
+
'product-discount': new ProductDiscountService(
|
|
185
|
+
projectRouter,
|
|
186
|
+
this._storage
|
|
187
|
+
),
|
|
183
188
|
'product-projection': new ProductProjectionService(
|
|
184
189
|
projectRouter,
|
|
185
190
|
this._storage
|
package/src/helpers.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { v4 as uuidv4 } from 'uuid'
|
|
2
|
+
import { ParsedQs } from 'qs'
|
|
3
|
+
import { Price } from '@commercetools/platform-sdk'
|
|
2
4
|
|
|
3
5
|
export const getBaseResourceProperties = () => {
|
|
4
6
|
return {
|
|
@@ -8,3 +10,16 @@ export const getBaseResourceProperties = () => {
|
|
|
8
10
|
version: 0,
|
|
9
11
|
}
|
|
10
12
|
}
|
|
13
|
+
|
|
14
|
+
export const QueryParamsAsArray = (
|
|
15
|
+
input: string | ParsedQs | string[] | ParsedQs[] | undefined
|
|
16
|
+
): string[] => {
|
|
17
|
+
if (input == undefined) {
|
|
18
|
+
return []
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (Array.isArray(input)) {
|
|
22
|
+
return input as string[]
|
|
23
|
+
}
|
|
24
|
+
return [input] as string[]
|
|
25
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { Product, ProductData } from '@commercetools/platform-sdk'
|
|
2
|
+
import { applyPriceSelector } from '../priceSelector'
|
|
3
|
+
import { parseFilterExpression } from './projectionSearchFilter'
|
|
4
|
+
|
|
5
|
+
describe('Search filter', () => {
|
|
6
|
+
const productData: ProductData = {
|
|
7
|
+
name: {
|
|
8
|
+
'nl-NL': 'test',
|
|
9
|
+
},
|
|
10
|
+
slug: {
|
|
11
|
+
'nl-NL': 'test',
|
|
12
|
+
},
|
|
13
|
+
variants: [],
|
|
14
|
+
searchKeywords: {},
|
|
15
|
+
categories: [],
|
|
16
|
+
masterVariant: {
|
|
17
|
+
id: 1,
|
|
18
|
+
sku: 'MYSKU',
|
|
19
|
+
attributes: [
|
|
20
|
+
{
|
|
21
|
+
name: 'Country',
|
|
22
|
+
value: {
|
|
23
|
+
key: 'NL',
|
|
24
|
+
label: {
|
|
25
|
+
de: 'niederlande',
|
|
26
|
+
en: 'netherlands',
|
|
27
|
+
nl: 'nederland',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'number',
|
|
33
|
+
value: 4,
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
prices: [
|
|
37
|
+
{
|
|
38
|
+
id: 'dummy-uuid',
|
|
39
|
+
value: {
|
|
40
|
+
type: 'centPrecision',
|
|
41
|
+
currencyCode: 'EUR',
|
|
42
|
+
centAmount: 1789,
|
|
43
|
+
fractionDigits: 2,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const exampleProduct: Product = {
|
|
51
|
+
id: '7401d82f-1378-47ba-996a-85beeb87ac87',
|
|
52
|
+
version: 2,
|
|
53
|
+
createdAt: '2022-07-22T10:02:40.851Z',
|
|
54
|
+
lastModifiedAt: '2022-07-22T10:02:44.427Z',
|
|
55
|
+
key: 'test-product',
|
|
56
|
+
productType: {
|
|
57
|
+
typeId: 'product-type',
|
|
58
|
+
id: 'b9b4b426-938b-4ccb-9f36-c6f933e8446e',
|
|
59
|
+
},
|
|
60
|
+
masterData: {
|
|
61
|
+
current: productData,
|
|
62
|
+
staged: productData,
|
|
63
|
+
published: true,
|
|
64
|
+
hasStagedChanges: false,
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const match = (pattern: string, product?: Product) => {
|
|
69
|
+
const matchFunc = parseFilterExpression(pattern, false)
|
|
70
|
+
const clone = JSON.parse(JSON.stringify(product ?? exampleProduct))
|
|
71
|
+
return {
|
|
72
|
+
isMatch: matchFunc(clone, false),
|
|
73
|
+
product: clone,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
test('by product key', async () => {
|
|
78
|
+
expect(match(`key:exists`).isMatch).toBeTruthy()
|
|
79
|
+
expect(match(`key:missing`).isMatch).toBeFalsy()
|
|
80
|
+
expect(match(`key:"test-product"`).isMatch).toBeTruthy()
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('by product type id', async () => {
|
|
84
|
+
expect(match(`productType.id:"b9b4b426-938b-4ccb-9f36-c6f933e8446e"`).isMatch).toBeTruthy()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('by SKU', async () => {
|
|
88
|
+
expect(match(`variants.sku:exists`).isMatch).toBeTruthy()
|
|
89
|
+
expect(match(`variants.sku:missing`).isMatch).toBeFalsy()
|
|
90
|
+
expect(match(`variants.sku:"MYSKU"`).isMatch).toBeTruthy()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('by attribute value', async () => {
|
|
94
|
+
expect(match(`variants.attributes.number:4`).isMatch).toBeTruthy()
|
|
95
|
+
expect(match(`variants.attributes.number:3,4`).isMatch).toBeTruthy()
|
|
96
|
+
expect(match(`variants.attributes.number:3,4,5`).isMatch).toBeTruthy()
|
|
97
|
+
expect(match(`variants.attributes.number:1,2,3,5`).isMatch).toBeFalsy()
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('by attribute range', async () => {
|
|
101
|
+
expect(
|
|
102
|
+
match(`variants.attributes.number:range (0 TO 5)`).isMatch
|
|
103
|
+
).toBeTruthy()
|
|
104
|
+
|
|
105
|
+
expect(
|
|
106
|
+
match(`variants.attributes.number:range (* TO 5)`).isMatch
|
|
107
|
+
).toBeTruthy()
|
|
108
|
+
|
|
109
|
+
expect(
|
|
110
|
+
match(`variants.attributes.number:range (* TO *)`).isMatch
|
|
111
|
+
).toBeTruthy()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('by attribute enum key', async () => {
|
|
115
|
+
expect(match(`variants.attributes.Country.key:"NL"`).isMatch).toBeTruthy()
|
|
116
|
+
expect(match(`variants.attributes.Country.key:"DE"`).isMatch).toBeFalsy()
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('by attribute enum key', async () => {
|
|
120
|
+
expect(match(`variants.attributes.Country.key:"NL"`).isMatch).toBeTruthy()
|
|
121
|
+
expect(match(`variants.attributes.Country.key:"DE"`).isMatch).toBeFalsy()
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('by price range', async () => {
|
|
125
|
+
expect(
|
|
126
|
+
match(`variants.price.centAmount:range (1500 TO 2000)`).isMatch
|
|
127
|
+
).toBeTruthy()
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('by scopedPrice range', async () => {
|
|
131
|
+
let result
|
|
132
|
+
let products: Product[]
|
|
133
|
+
|
|
134
|
+
// No currency given
|
|
135
|
+
result = match(`variants.scopedPrice.value.centAmount:range (1500 TO 2000)`)
|
|
136
|
+
expect(result.isMatch).toBeFalsy()
|
|
137
|
+
|
|
138
|
+
// Currency match
|
|
139
|
+
products = [JSON.parse(JSON.stringify(exampleProduct))]
|
|
140
|
+
applyPriceSelector(products, { currency: 'EUR' })
|
|
141
|
+
|
|
142
|
+
result = match(
|
|
143
|
+
`variants.scopedPrice.value.centAmount:range (1500 TO 2000)`,
|
|
144
|
+
products[0]
|
|
145
|
+
)
|
|
146
|
+
expect(result.isMatch).toBeTruthy()
|
|
147
|
+
expect(result.product).toMatchObject({
|
|
148
|
+
masterData: {
|
|
149
|
+
current: {
|
|
150
|
+
masterVariant: {
|
|
151
|
+
sku: 'MYSKU',
|
|
152
|
+
scopedPrice: { value: { centAmount: 1789 } },
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// Currency mismatch
|
|
159
|
+
products = [JSON.parse(JSON.stringify(exampleProduct))]
|
|
160
|
+
applyPriceSelector(products, { currency: 'USD' })
|
|
161
|
+
|
|
162
|
+
result = match(
|
|
163
|
+
`variants.scopedPrice.value.centAmount:range (1500 TO 2000)`,
|
|
164
|
+
products[0]
|
|
165
|
+
)
|
|
166
|
+
expect(result.isMatch).toBeFalsy()
|
|
167
|
+
|
|
168
|
+
// Price has no country so mismatch
|
|
169
|
+
products = [JSON.parse(JSON.stringify(exampleProduct))]
|
|
170
|
+
applyPriceSelector(products, { currency: 'EUR', country: 'NL' })
|
|
171
|
+
result = match(
|
|
172
|
+
`variants.scopedPrice.value.centAmount:range (1500 TO 2000)`,
|
|
173
|
+
products[0]
|
|
174
|
+
)
|
|
175
|
+
expect(result.isMatch).toBeFalsy()
|
|
176
|
+
})
|
|
177
|
+
})
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This module implements the commercetools product projection filter expression.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Product, ProductVariant } from '@commercetools/platform-sdk'
|
|
6
|
+
import perplex from 'perplex'
|
|
7
|
+
import Parser from 'pratt'
|
|
8
|
+
import { Writable } from '../types'
|
|
9
|
+
|
|
10
|
+
type MatchFunc = (target: any) => boolean
|
|
11
|
+
|
|
12
|
+
type ProductFilter = (
|
|
13
|
+
p: Writable<Product>,
|
|
14
|
+
markMatchingVariants: boolean
|
|
15
|
+
) => boolean
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Returns a function (ProductFilter).
|
|
19
|
+
* NOTE: The filter can alter the resources in-place (FIXME)
|
|
20
|
+
*/
|
|
21
|
+
export const parseFilterExpression = (
|
|
22
|
+
filter: string,
|
|
23
|
+
staged: boolean
|
|
24
|
+
): ProductFilter => {
|
|
25
|
+
const exprFunc = generateMatchFunc(filter)
|
|
26
|
+
const [source] = filter.split(':', 1)
|
|
27
|
+
|
|
28
|
+
if (source.startsWith('variants.')) {
|
|
29
|
+
return filterVariants(source, staged, exprFunc)
|
|
30
|
+
}
|
|
31
|
+
return filterProduct(source, exprFunc)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const getLexer = (value: string) => {
|
|
35
|
+
return new perplex(value)
|
|
36
|
+
.token('MISSING', /missing(?![-_a-z0-9]+)/i)
|
|
37
|
+
.token('EXISTS', /exists(?![-_a-z0-9]+)/i)
|
|
38
|
+
.token('RANGE', /range(?![-_a-z0-9]+)/i)
|
|
39
|
+
.token('TO', /to(?![-_a-z0-9]+)/i)
|
|
40
|
+
.token('IDENTIFIER', /[-_\.a-z]+/i)
|
|
41
|
+
|
|
42
|
+
.token('FLOAT', /\d+\.\d+/)
|
|
43
|
+
.token('INT', /\d+/)
|
|
44
|
+
.token('STRING', /"((?:\\.|[^"\\])*)"/)
|
|
45
|
+
.token('STRING', /'((?:\\.|[^'\\])*)'/)
|
|
46
|
+
|
|
47
|
+
.token('COMMA', ',')
|
|
48
|
+
.token('STAR', '*')
|
|
49
|
+
.token('(', '(')
|
|
50
|
+
.token(':', ':')
|
|
51
|
+
.token(')', ')')
|
|
52
|
+
.token('"', '"')
|
|
53
|
+
.token('WS', /\s+/, true) // skip
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const generateMatchFunc = (filter: string): MatchFunc => {
|
|
57
|
+
const lexer = getLexer(filter)
|
|
58
|
+
const parser = new Parser(lexer)
|
|
59
|
+
.builder()
|
|
60
|
+
.nud('IDENTIFIER', 100, t => {
|
|
61
|
+
return t.token.match
|
|
62
|
+
})
|
|
63
|
+
.led(':', 100, ({ left, bp }) => {
|
|
64
|
+
const expr = parser.parse({ terminals: [bp - 1] })
|
|
65
|
+
|
|
66
|
+
if (Array.isArray(expr)) {
|
|
67
|
+
return (obj: any): boolean => {
|
|
68
|
+
return expr.includes(obj)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (typeof expr === 'function') {
|
|
72
|
+
return (obj: any): boolean => {
|
|
73
|
+
return expr(obj)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return (obj: any): boolean => {
|
|
77
|
+
return obj === expr
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
.nud('STRING', 20, t => {
|
|
81
|
+
// @ts-ignore
|
|
82
|
+
return t.token.groups[1]
|
|
83
|
+
})
|
|
84
|
+
.nud('INT', 5, t => {
|
|
85
|
+
// @ts-ignore
|
|
86
|
+
return parseInt(t.token.match, 10)
|
|
87
|
+
})
|
|
88
|
+
.nud('STAR', 5, t => {
|
|
89
|
+
return null
|
|
90
|
+
})
|
|
91
|
+
.nud('EXISTS', 10, ({ bp }) => {
|
|
92
|
+
return (val: any) => {
|
|
93
|
+
return val !== undefined
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
.nud('MISSING', 10, ({ bp }) => {
|
|
97
|
+
return (val: any) => {
|
|
98
|
+
return val === undefined
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
.led('COMMA', 200, ({ left, token, bp }) => {
|
|
102
|
+
const expr: any = parser.parse({ terminals: [bp - 1] })
|
|
103
|
+
if (Array.isArray(expr)) {
|
|
104
|
+
return [left, ...expr]
|
|
105
|
+
} else {
|
|
106
|
+
return [left, expr]
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
.bp(')', 0)
|
|
110
|
+
.led('TO', 20, ({ left, bp }) => {
|
|
111
|
+
const expr: any = parser.parse({ terminals: [bp - 1] })
|
|
112
|
+
return [left, expr]
|
|
113
|
+
})
|
|
114
|
+
.nud('RANGE', 20, ({ bp }) => {
|
|
115
|
+
lexer.next() // Skip over opening parthensis
|
|
116
|
+
const [start, stop] = parser.parse()
|
|
117
|
+
console.log(start, stop)
|
|
118
|
+
if (start !== null && stop !== null ) {
|
|
119
|
+
return (obj: any): boolean => {
|
|
120
|
+
return obj >= start && obj <= stop
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else if (start === null && stop !== null) {
|
|
124
|
+
return (obj: any): boolean => {
|
|
125
|
+
return obj <= stop
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else if (start !== null && stop === null) {
|
|
129
|
+
return (obj: any): boolean => {
|
|
130
|
+
return obj >= start
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
return (obj: any): boolean => {
|
|
134
|
+
return true
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
.build()
|
|
140
|
+
|
|
141
|
+
const result = parser.parse()
|
|
142
|
+
|
|
143
|
+
if (typeof result !== 'function') {
|
|
144
|
+
const lines = filter.split('\n')
|
|
145
|
+
const column = lines[lines.length - 1].length
|
|
146
|
+
throw new Error(`Syntax error while parsing '${filter}'.`)
|
|
147
|
+
}
|
|
148
|
+
return result
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const filterProduct = (
|
|
152
|
+
source: string,
|
|
153
|
+
exprFunc: MatchFunc
|
|
154
|
+
): ProductFilter => {
|
|
155
|
+
return (p: Product, markMatchingVariants: boolean): boolean => {
|
|
156
|
+
const value = nestedLookup(p, source)
|
|
157
|
+
return exprFunc(value)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const filterVariants = (
|
|
162
|
+
source: string,
|
|
163
|
+
staged: boolean,
|
|
164
|
+
exprFunc: MatchFunc
|
|
165
|
+
): ProductFilter => {
|
|
166
|
+
return (p: Product, markMatchingVariants: boolean): boolean => {
|
|
167
|
+
const [, ...paths] = source.split('.')
|
|
168
|
+
const path = paths.join('.')
|
|
169
|
+
|
|
170
|
+
const variants = getVariants(p, staged) as Writable<ProductVariant>[]
|
|
171
|
+
for (const variant of variants) {
|
|
172
|
+
const value = resolveVariantValue(variant, path)
|
|
173
|
+
|
|
174
|
+
if (exprFunc(value)) {
|
|
175
|
+
|
|
176
|
+
// If markMatchingVariants parameter is true those ProductVariants that
|
|
177
|
+
// match the search query have the additional field isMatchingVariant
|
|
178
|
+
// set to true. For the other variants in the same product projection
|
|
179
|
+
// this field is set to false.
|
|
180
|
+
if (markMatchingVariants) {
|
|
181
|
+
variants.forEach(v => v.isMatchingVariant = false)
|
|
182
|
+
variant.isMatchingVariant = true
|
|
183
|
+
}
|
|
184
|
+
return true
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return false
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const resolveVariantValue = (obj: ProductVariant, path: string): any => {
|
|
193
|
+
if (path === undefined) {
|
|
194
|
+
return obj
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (path.startsWith('attributes.')) {
|
|
198
|
+
const [, attrName, ...rest] = path.split('.')
|
|
199
|
+
if (!obj.attributes) {
|
|
200
|
+
return undefined
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
for (const attr of obj.attributes) {
|
|
204
|
+
if (attr.name === attrName) {
|
|
205
|
+
return nestedLookup(attr.value, rest.join('.'))
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (path === 'price.centAmount') {
|
|
211
|
+
return obj.prices && obj.prices.length > 0
|
|
212
|
+
? obj.prices[0].value.centAmount
|
|
213
|
+
: undefined
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return nestedLookup(obj, path)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const nestedLookup = (obj: any, path: string): any => {
|
|
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[] => {
|
|
240
|
+
return [
|
|
241
|
+
staged
|
|
242
|
+
? p.masterData.staged?.masterVariant
|
|
243
|
+
: p.masterData.current?.masterVariant,
|
|
244
|
+
...(staged
|
|
245
|
+
? p.masterData.staged?.variants
|
|
246
|
+
: p.masterData.current?.variants),
|
|
247
|
+
]
|
|
248
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { ProductData, Product } from '@commercetools/platform-sdk'
|
|
2
|
+
import { applyPriceSelector } from './priceSelector'
|
|
3
|
+
|
|
4
|
+
describe('priceSelector', () => {
|
|
5
|
+
let product: Product
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
const productData: ProductData = {
|
|
9
|
+
name: {
|
|
10
|
+
'nl-NL': 'test',
|
|
11
|
+
},
|
|
12
|
+
slug: {
|
|
13
|
+
'nl-NL': 'test',
|
|
14
|
+
},
|
|
15
|
+
variants: [],
|
|
16
|
+
searchKeywords: {},
|
|
17
|
+
categories: [],
|
|
18
|
+
masterVariant: {
|
|
19
|
+
id: 1,
|
|
20
|
+
sku: 'MYSKU',
|
|
21
|
+
attributes: [
|
|
22
|
+
{
|
|
23
|
+
name: 'Country',
|
|
24
|
+
value: {
|
|
25
|
+
key: 'NL',
|
|
26
|
+
label: {
|
|
27
|
+
de: 'niederlande',
|
|
28
|
+
en: 'netherlands',
|
|
29
|
+
nl: 'nederland',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'number',
|
|
35
|
+
value: 4,
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
prices: [
|
|
39
|
+
{
|
|
40
|
+
id: 'dummy-uuid',
|
|
41
|
+
value: {
|
|
42
|
+
type: 'centPrecision',
|
|
43
|
+
currencyCode: 'EUR',
|
|
44
|
+
centAmount: 1789,
|
|
45
|
+
fractionDigits: 2,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
product = {
|
|
53
|
+
id: '7401d82f-1378-47ba-996a-85beeb87ac87',
|
|
54
|
+
version: 2,
|
|
55
|
+
createdAt: '2022-07-22T10:02:40.851Z',
|
|
56
|
+
lastModifiedAt: '2022-07-22T10:02:44.427Z',
|
|
57
|
+
productType: {
|
|
58
|
+
typeId: 'product-type',
|
|
59
|
+
id: 'b9b4b426-938b-4ccb-9f36-c6f933e8446e',
|
|
60
|
+
},
|
|
61
|
+
masterData: {
|
|
62
|
+
current: productData,
|
|
63
|
+
staged: productData,
|
|
64
|
+
published: true,
|
|
65
|
+
hasStagedChanges: false,
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('currency (match)', async () => {
|
|
71
|
+
applyPriceSelector([product], { currency: 'EUR' })
|
|
72
|
+
|
|
73
|
+
expect(product).toMatchObject({
|
|
74
|
+
masterData: {
|
|
75
|
+
current: {
|
|
76
|
+
masterVariant: {
|
|
77
|
+
sku: 'MYSKU',
|
|
78
|
+
scopedPrice: { value: { centAmount: 1789 } },
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
staged: {
|
|
82
|
+
masterVariant: {
|
|
83
|
+
sku: 'MYSKU',
|
|
84
|
+
scopedPrice: { value: { centAmount: 1789 } },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('currency, country (no match)', async () => {
|
|
92
|
+
applyPriceSelector([product], { currency: 'EUR', country: 'US' })
|
|
93
|
+
expect(product.masterData.current.masterVariant.scopedPrice).toBeUndefined()
|
|
94
|
+
expect(product.masterData.staged.masterVariant.scopedPrice).toBeUndefined()
|
|
95
|
+
})
|
|
96
|
+
})
|