@labdigital/commercetools-mock 0.6.5 → 0.8.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 +409 -3
- package/dist/index.global.js +49983 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +4835 -6
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +4803 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +36 -17
- package/src/ctMock.ts +5 -0
- package/src/helpers.ts +39 -0
- package/src/lib/projectionSearchFilter.test.ts +183 -0
- package/src/lib/projectionSearchFilter.ts +347 -0
- package/src/priceSelector.test.ts +96 -0
- package/src/priceSelector.ts +109 -0
- package/src/product-projection-search.ts +345 -0
- package/src/projectAPI.ts +19 -20
- 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/helpers.ts +46 -4
- package/src/repositories/product-discount.ts +181 -0
- package/src/repositories/product-projection.ts +30 -59
- 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/custom-object.test.ts +2 -2
- package/src/services/product-discount.ts +33 -0
- package/src/services/product-projection.test.ts +329 -107
- package/src/services/product.test.ts +12 -0
- package/src/storage.ts +116 -58
- package/src/types.ts +9 -2
- package/dist/commercetools-mock.cjs.development.js +0 -4382
- 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 -4374
- 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/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.8.0",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"typings": "dist/index.d.ts",
|
|
@@ -7,17 +7,14 @@
|
|
|
7
7
|
"dist",
|
|
8
8
|
"src"
|
|
9
9
|
],
|
|
10
|
-
"
|
|
11
|
-
"
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"require": "./dist/index.js",
|
|
13
|
+
"import": "./dist/index.mjs"
|
|
14
|
+
}
|
|
12
15
|
},
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"server": "nodemon --watch src --exec 'node -r esbuild-register' src/server.ts",
|
|
16
|
-
"generate": "node -r esbuild-register generate-validation.ts",
|
|
17
|
-
"build": "tsdx build --target=node",
|
|
18
|
-
"test": "tsdx test",
|
|
19
|
-
"lint": "tsdx lint",
|
|
20
|
-
"prepare": "tsdx build"
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=14"
|
|
21
18
|
},
|
|
22
19
|
"prettier": {
|
|
23
20
|
"printWidth": 80,
|
|
@@ -29,36 +26,58 @@
|
|
|
29
26
|
"author": "Michael van Tellingen",
|
|
30
27
|
"module": "dist/commercetools--mock.esm.js",
|
|
31
28
|
"devDependencies": {
|
|
32
|
-
"@babel/preset-
|
|
33
|
-
"@
|
|
29
|
+
"@babel/preset-env": "^7.18.9",
|
|
30
|
+
"@babel/preset-typescript": "^7.18.6",
|
|
31
|
+
"@commercetools/platform-sdk": "2.8.0",
|
|
34
32
|
"@types/basic-auth": "^1.1.3",
|
|
33
|
+
"@types/body-parser": "^1.19.2",
|
|
35
34
|
"@types/deep-equal": "^1.0.1",
|
|
36
35
|
"@types/express": "^4.17.13",
|
|
36
|
+
"@types/express-serve-static-core": "^4.17.29",
|
|
37
|
+
"@types/jest": "^28.1.6",
|
|
37
38
|
"@types/morgan": "^1.9.3",
|
|
39
|
+
"@types/node": "*",
|
|
40
|
+
"@types/qs": "^6.9.7",
|
|
38
41
|
"@types/supertest": "^2.0.11",
|
|
39
42
|
"@types/uuid": "^8.3.4",
|
|
43
|
+
"esbuild": "^0.14.50",
|
|
40
44
|
"esbuild-register": "^3.3.1",
|
|
45
|
+
"eslint": "^8.20.0",
|
|
41
46
|
"got": "^11.8.3",
|
|
42
47
|
"husky": "^7.0.4",
|
|
48
|
+
"jest": "^28.1.3",
|
|
43
49
|
"nodemon": "^2.0.15",
|
|
50
|
+
"timekeeper": "^2.2.0",
|
|
44
51
|
"ts-node": "^10.4.0",
|
|
45
|
-
"
|
|
52
|
+
"tsc": "^2.0.4",
|
|
46
53
|
"tslib": "^2.3.1",
|
|
47
|
-
"
|
|
54
|
+
"tsup": "^6.2.0",
|
|
55
|
+
"typescript": "^4.7.4"
|
|
48
56
|
},
|
|
49
57
|
"dependencies": {
|
|
58
|
+
"@types/lodash": "^4.14.182",
|
|
50
59
|
"ajv": "^8.8.2",
|
|
51
60
|
"ajv-formats": "^2.1.1",
|
|
52
61
|
"basic-auth": "^2.0.1",
|
|
62
|
+
"body-parser": "^1.20.0",
|
|
53
63
|
"deep-equal": "^2.0.5",
|
|
54
64
|
"express": "^4.17.2",
|
|
65
|
+
"lodash": "^4.17.21",
|
|
55
66
|
"morgan": "^1.10.0",
|
|
56
67
|
"nock": "^13.2.1",
|
|
57
68
|
"perplex": "^0.11.0",
|
|
58
69
|
"pratt": "^0.7.0",
|
|
59
|
-
"supertest": "^6.1.6"
|
|
70
|
+
"supertest": "^6.1.6",
|
|
71
|
+
"uuid": "^8.3.2"
|
|
60
72
|
},
|
|
61
73
|
"peerDependencies": {
|
|
62
74
|
"@commercetools/platform-sdk": "^2.4.1"
|
|
75
|
+
},
|
|
76
|
+
"scripts": {
|
|
77
|
+
"start": "nodemon --watch src --exec 'node -r esbuild-register' src/server.ts",
|
|
78
|
+
"generate": "node -r esbuild-register generate-validation.ts",
|
|
79
|
+
"build": "tsup",
|
|
80
|
+
"test": "jest test --coverage",
|
|
81
|
+
"lint": "eslint"
|
|
63
82
|
}
|
|
64
|
-
}
|
|
83
|
+
}
|
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,40 @@ export const getBaseResourceProperties = () => {
|
|
|
8
10
|
version: 0,
|
|
9
11
|
}
|
|
10
12
|
}
|
|
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
|
+
|
|
38
|
+
export const QueryParamsAsArray = (
|
|
39
|
+
input: string | ParsedQs | string[] | ParsedQs[] | undefined
|
|
40
|
+
): string[] => {
|
|
41
|
+
if (input == undefined) {
|
|
42
|
+
return []
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (Array.isArray(input)) {
|
|
46
|
+
return input as string[]
|
|
47
|
+
}
|
|
48
|
+
return [input] as string[]
|
|
49
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
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 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
|
+
|
|
136
|
+
test('by scopedPrice range', async () => {
|
|
137
|
+
let result
|
|
138
|
+
let products: Product[]
|
|
139
|
+
|
|
140
|
+
// No currency given
|
|
141
|
+
result = match(`variants.scopedPrice.value.centAmount:range (1500 TO 2000)`)
|
|
142
|
+
expect(result.isMatch).toBeFalsy()
|
|
143
|
+
|
|
144
|
+
// Currency match
|
|
145
|
+
products = [JSON.parse(JSON.stringify(exampleProduct))]
|
|
146
|
+
applyPriceSelector(products, { currency: 'EUR' })
|
|
147
|
+
|
|
148
|
+
result = match(
|
|
149
|
+
`variants.scopedPrice.value.centAmount:range (1500 TO 2000)`,
|
|
150
|
+
products[0]
|
|
151
|
+
)
|
|
152
|
+
expect(result.isMatch).toBeTruthy()
|
|
153
|
+
expect(result.product).toMatchObject({
|
|
154
|
+
masterData: {
|
|
155
|
+
current: {
|
|
156
|
+
masterVariant: {
|
|
157
|
+
sku: 'MYSKU',
|
|
158
|
+
scopedPrice: { value: { centAmount: 1789 } },
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
// Currency mismatch
|
|
165
|
+
products = [JSON.parse(JSON.stringify(exampleProduct))]
|
|
166
|
+
applyPriceSelector(products, { currency: 'USD' })
|
|
167
|
+
|
|
168
|
+
result = match(
|
|
169
|
+
`variants.scopedPrice.value.centAmount:range (1500 TO 2000)`,
|
|
170
|
+
products[0]
|
|
171
|
+
)
|
|
172
|
+
expect(result.isMatch).toBeFalsy()
|
|
173
|
+
|
|
174
|
+
// Price has no country so mismatch
|
|
175
|
+
products = [JSON.parse(JSON.stringify(exampleProduct))]
|
|
176
|
+
applyPriceSelector(products, { currency: 'EUR', country: 'NL' })
|
|
177
|
+
result = match(
|
|
178
|
+
`variants.scopedPrice.value.centAmount:range (1500 TO 2000)`,
|
|
179
|
+
products[0]
|
|
180
|
+
)
|
|
181
|
+
expect(result.isMatch).toBeFalsy()
|
|
182
|
+
})
|
|
183
|
+
})
|
|
@@ -0,0 +1,347 @@
|
|
|
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 { nestedLookup } from '../helpers'
|
|
9
|
+
import { Writable } from '../types'
|
|
10
|
+
|
|
11
|
+
type MatchFunc = (target: any) => boolean
|
|
12
|
+
|
|
13
|
+
type ProductFilter = (
|
|
14
|
+
p: Writable<Product>,
|
|
15
|
+
markMatchingVariants: boolean
|
|
16
|
+
) => boolean
|
|
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
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Returns a function (ProductFilter).
|
|
57
|
+
* NOTE: The filter can alter the resources in-place (FIXME)
|
|
58
|
+
*/
|
|
59
|
+
export const parseFilterExpression = (
|
|
60
|
+
filter: string,
|
|
61
|
+
staged: boolean
|
|
62
|
+
): ProductFilter => {
|
|
63
|
+
const exprFunc = generateMatchFunc(filter)
|
|
64
|
+
const [source] = filter.split(':', 1)
|
|
65
|
+
|
|
66
|
+
if (source.startsWith('variants.')) {
|
|
67
|
+
return filterVariants(source, staged, exprFunc)
|
|
68
|
+
}
|
|
69
|
+
return filterProduct(source, exprFunc)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const getLexer = (value: string) => {
|
|
73
|
+
return new perplex(value)
|
|
74
|
+
.token('MISSING', /missing(?![-_a-z0-9]+)/i)
|
|
75
|
+
.token('EXISTS', /exists(?![-_a-z0-9]+)/i)
|
|
76
|
+
.token('RANGE', /range(?![-_a-z0-9]+)/i)
|
|
77
|
+
.token('TO', /to(?![-_a-z0-9]+)/i)
|
|
78
|
+
.token('IDENTIFIER', /[-_\.a-z]+/i)
|
|
79
|
+
|
|
80
|
+
.token('FLOAT', /\d+\.\d+/)
|
|
81
|
+
.token('INT', /\d+/)
|
|
82
|
+
.token('STRING', /"((?:\\.|[^"\\])*)"/)
|
|
83
|
+
.token('STRING', /'((?:\\.|[^'\\])*)'/)
|
|
84
|
+
|
|
85
|
+
.token('COMMA', ',')
|
|
86
|
+
.token('STAR', '*')
|
|
87
|
+
.token('(', '(')
|
|
88
|
+
.token(':', ':')
|
|
89
|
+
.token(')', ')')
|
|
90
|
+
.token('"', '"')
|
|
91
|
+
.token('WS', /\s+/, true) // skip
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const parseFilter = (filter: string): ExpressionSet => {
|
|
95
|
+
const lexer = getLexer(filter)
|
|
96
|
+
const parser = new Parser(lexer)
|
|
97
|
+
.builder()
|
|
98
|
+
.nud('IDENTIFIER', 100, t => {
|
|
99
|
+
return t.token.match
|
|
100
|
+
})
|
|
101
|
+
.led(':', 100, ({ left, bp }) => {
|
|
102
|
+
let parsed: any = parser.parse({ terminals: [bp - 1] })
|
|
103
|
+
let expressions: RangeExpression[] | FilterExpression[] | Symbol[]
|
|
104
|
+
expressions = !Array.isArray(parsed) ? [parsed] : parsed
|
|
105
|
+
|
|
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')
|
|
110
|
+
}
|
|
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
|
+
})
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
source: left,
|
|
136
|
+
type: expressions[0].type,
|
|
137
|
+
children: expressions,
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
.nud('STRING', 20, t => {
|
|
141
|
+
return {
|
|
142
|
+
type: 'Symbol',
|
|
143
|
+
kind: 'string',
|
|
144
|
+
// @ts-ignore
|
|
145
|
+
value: t.token.groups[1],
|
|
146
|
+
} as Symbol
|
|
147
|
+
})
|
|
148
|
+
.nud('INT', 5, t => {
|
|
149
|
+
return {
|
|
150
|
+
type: 'Symbol',
|
|
151
|
+
kind: 'int',
|
|
152
|
+
value: parseInt(t.token.match, 10),
|
|
153
|
+
} as Symbol
|
|
154
|
+
})
|
|
155
|
+
.nud('STAR', 5, t => {
|
|
156
|
+
return {
|
|
157
|
+
type: 'Symbol',
|
|
158
|
+
kind: 'any',
|
|
159
|
+
value: null,
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
.nud('EXISTS', 10, ({ bp }) => {
|
|
163
|
+
return {
|
|
164
|
+
type: 'FilterExpression',
|
|
165
|
+
match: (obj: any): boolean => {
|
|
166
|
+
return obj !== undefined
|
|
167
|
+
},
|
|
168
|
+
} as FilterExpression
|
|
169
|
+
})
|
|
170
|
+
.nud('MISSING', 10, ({ bp }) => {
|
|
171
|
+
return {
|
|
172
|
+
type: 'FilterExpression',
|
|
173
|
+
match: (obj: any): boolean => {
|
|
174
|
+
return obj === undefined
|
|
175
|
+
},
|
|
176
|
+
} as FilterExpression
|
|
177
|
+
})
|
|
178
|
+
.led('COMMA', 200, ({ left, token, bp }) => {
|
|
179
|
+
const expr: any = parser.parse({ terminals: [bp - 1] })
|
|
180
|
+
if (Array.isArray(expr)) {
|
|
181
|
+
return [left, ...expr]
|
|
182
|
+
} else {
|
|
183
|
+
return [left, expr]
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
.nud('(', 100, t => {
|
|
187
|
+
const expr: any = parser.parse({ terminals: [')'] })
|
|
188
|
+
lexer.expect(')')
|
|
189
|
+
return expr
|
|
190
|
+
})
|
|
191
|
+
.bp(')', 0)
|
|
192
|
+
.led('TO', 20, ({ left, bp }) => {
|
|
193
|
+
const expr: any = parser.parse({ terminals: [bp - 1] })
|
|
194
|
+
return {
|
|
195
|
+
start: left.value,
|
|
196
|
+
stop: expr.value,
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
.nud('RANGE', 20, ({ bp }) => {
|
|
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]
|
|
206
|
+
}
|
|
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
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
type: 'RangeExpression',
|
|
233
|
+
start: range.start,
|
|
234
|
+
stop: range.stop,
|
|
235
|
+
match: func,
|
|
236
|
+
} as RangeExpression
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
.build()
|
|
240
|
+
|
|
241
|
+
return parser.parse()
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const generateMatchFunc = (filter: string) => {
|
|
245
|
+
const result = parseFilter(filter)
|
|
246
|
+
if (!result) {
|
|
247
|
+
const lines = filter.split('\n')
|
|
248
|
+
const column = lines[lines.length - 1].length
|
|
249
|
+
throw new Error(`Syntax error while parsing '${filter}'.`)
|
|
250
|
+
}
|
|
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
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
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 => {
|
|
272
|
+
return (p: Product, markMatchingVariants: boolean): boolean => {
|
|
273
|
+
const value = nestedLookup(p, source)
|
|
274
|
+
return exprFunc(value)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const filterVariants = (
|
|
279
|
+
source: string,
|
|
280
|
+
staged: boolean,
|
|
281
|
+
exprFunc: MatchFunc
|
|
282
|
+
): ProductFilter => {
|
|
283
|
+
return (p: Product, markMatchingVariants: boolean): boolean => {
|
|
284
|
+
const [, ...paths] = source.split('.')
|
|
285
|
+
const path = paths.join('.')
|
|
286
|
+
|
|
287
|
+
const variants = getVariants(p, staged) as Writable<ProductVariant>[]
|
|
288
|
+
for (const variant of variants) {
|
|
289
|
+
const value = resolveVariantValue(variant, path)
|
|
290
|
+
|
|
291
|
+
if (exprFunc(value)) {
|
|
292
|
+
// If markMatchingVariants parameter is true those ProductVariants that
|
|
293
|
+
// match the search query have the additional field isMatchingVariant
|
|
294
|
+
// set to true. For the other variants in the same product projection
|
|
295
|
+
// this field is set to false.
|
|
296
|
+
if (markMatchingVariants) {
|
|
297
|
+
variants.forEach(v => (v.isMatchingVariant = false))
|
|
298
|
+
variant.isMatchingVariant = true
|
|
299
|
+
}
|
|
300
|
+
return true
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return false
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export const resolveVariantValue = (obj: ProductVariant, path: string): any => {
|
|
309
|
+
if (path === undefined) {
|
|
310
|
+
return obj
|
|
311
|
+
}
|
|
312
|
+
if (path.startsWith('variants.')) {
|
|
313
|
+
path = path.substring(path.indexOf('.') + 1)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (path.startsWith('attributes.')) {
|
|
317
|
+
const [, attrName, ...rest] = path.split('.')
|
|
318
|
+
if (!obj.attributes) {
|
|
319
|
+
return undefined
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
for (const attr of obj.attributes) {
|
|
323
|
+
if (attr.name === attrName) {
|
|
324
|
+
return nestedLookup(attr.value, rest.join('.'))
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (path === 'price.centAmount') {
|
|
330
|
+
return obj.prices && obj.prices.length > 0
|
|
331
|
+
? obj.prices[0].value.centAmount
|
|
332
|
+
: undefined
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return nestedLookup(obj, path)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export const getVariants = (p: Product, staged: boolean): ProductVariant[] => {
|
|
339
|
+
return [
|
|
340
|
+
staged
|
|
341
|
+
? p.masterData.staged?.masterVariant
|
|
342
|
+
: p.masterData.current?.masterVariant,
|
|
343
|
+
...(staged
|
|
344
|
+
? p.masterData.staged?.variants
|
|
345
|
+
: p.masterData.current?.variants),
|
|
346
|
+
]
|
|
347
|
+
}
|