@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.
Files changed (118) hide show
  1. package/dist/index.d.ts +409 -3
  2. package/dist/index.global.js +49983 -0
  3. package/dist/index.global.js.map +1 -0
  4. package/dist/index.js +4835 -6
  5. package/dist/index.js.map +1 -0
  6. package/dist/index.mjs +4803 -0
  7. package/dist/index.mjs.map +1 -0
  8. package/package.json +36 -17
  9. package/src/ctMock.ts +5 -0
  10. package/src/helpers.ts +39 -0
  11. package/src/lib/projectionSearchFilter.test.ts +183 -0
  12. package/src/lib/projectionSearchFilter.ts +347 -0
  13. package/src/priceSelector.test.ts +96 -0
  14. package/src/priceSelector.ts +109 -0
  15. package/src/product-projection-search.ts +345 -0
  16. package/src/projectAPI.ts +19 -20
  17. package/src/repositories/category.ts +36 -0
  18. package/src/repositories/channel.ts +104 -0
  19. package/src/repositories/customer-group.ts +37 -0
  20. package/src/repositories/discount-code.ts +37 -0
  21. package/src/repositories/helpers.ts +46 -4
  22. package/src/repositories/product-discount.ts +181 -0
  23. package/src/repositories/product-projection.ts +30 -59
  24. package/src/repositories/product-type.ts +88 -6
  25. package/src/repositories/product.ts +49 -9
  26. package/src/repositories/shipping-method.ts +31 -0
  27. package/src/repositories/store.ts +43 -3
  28. package/src/repositories/type.ts +19 -0
  29. package/src/services/custom-object.test.ts +2 -2
  30. package/src/services/product-discount.ts +33 -0
  31. package/src/services/product-projection.test.ts +329 -107
  32. package/src/services/product.test.ts +12 -0
  33. package/src/storage.ts +116 -58
  34. package/src/types.ts +9 -2
  35. package/dist/commercetools-mock.cjs.development.js +0 -4382
  36. package/dist/commercetools-mock.cjs.development.js.map +0 -1
  37. package/dist/commercetools-mock.cjs.production.min.js +0 -2
  38. package/dist/commercetools-mock.cjs.production.min.js.map +0 -1
  39. package/dist/commercetools-mock.esm.js +0 -4374
  40. package/dist/commercetools-mock.esm.js.map +0 -1
  41. package/dist/constants.d.ts +0 -2
  42. package/dist/ctMock.d.ts +0 -32
  43. package/dist/exceptions.d.ts +0 -12
  44. package/dist/helpers.d.ts +0 -6
  45. package/dist/lib/expandParser.d.ts +0 -15
  46. package/dist/lib/filterParser.d.ts +0 -1
  47. package/dist/lib/haversine.d.ts +0 -8
  48. package/dist/lib/masking.d.ts +0 -1
  49. package/dist/lib/predicateParser.d.ts +0 -11
  50. package/dist/lib/proxy.d.ts +0 -1
  51. package/dist/oauth/errors.d.ts +0 -8
  52. package/dist/oauth/helpers.d.ts +0 -2
  53. package/dist/oauth/server.d.ts +0 -12
  54. package/dist/oauth/store.d.ts +0 -14
  55. package/dist/projectAPI.d.ts +0 -12
  56. package/dist/repositories/abstract.d.ts +0 -33
  57. package/dist/repositories/cart-discount.d.ts +0 -9
  58. package/dist/repositories/cart.d.ts +0 -21
  59. package/dist/repositories/category.d.ts +0 -18
  60. package/dist/repositories/channel.d.ts +0 -6
  61. package/dist/repositories/custom-object.d.ts +0 -8
  62. package/dist/repositories/customer-group.d.ts +0 -11
  63. package/dist/repositories/customer.d.ts +0 -11
  64. package/dist/repositories/discount-code.d.ts +0 -8
  65. package/dist/repositories/errors.d.ts +0 -2
  66. package/dist/repositories/extension.d.ts +0 -8
  67. package/dist/repositories/helpers.d.ts +0 -10
  68. package/dist/repositories/inventory-entry.d.ts +0 -14
  69. package/dist/repositories/my-order.d.ts +0 -6
  70. package/dist/repositories/order.d.ts +0 -26
  71. package/dist/repositories/payment.d.ts +0 -23
  72. package/dist/repositories/product-projection.d.ts +0 -10
  73. package/dist/repositories/product-type.d.ts +0 -10
  74. package/dist/repositories/product.d.ts +0 -11
  75. package/dist/repositories/project.d.ts +0 -8
  76. package/dist/repositories/shipping-method.d.ts +0 -10
  77. package/dist/repositories/shopping-list.d.ts +0 -6
  78. package/dist/repositories/state.d.ts +0 -8
  79. package/dist/repositories/store.d.ts +0 -10
  80. package/dist/repositories/subscription.d.ts +0 -6
  81. package/dist/repositories/tax-category.d.ts +0 -10
  82. package/dist/repositories/type.d.ts +0 -8
  83. package/dist/repositories/zone.d.ts +0 -8
  84. package/dist/server.d.ts +0 -1
  85. package/dist/services/abstract.d.ts +0 -20
  86. package/dist/services/cart-discount.d.ts +0 -9
  87. package/dist/services/cart.d.ts +0 -12
  88. package/dist/services/category.d.ts +0 -9
  89. package/dist/services/channel.d.ts +0 -9
  90. package/dist/services/custom-object.d.ts +0 -13
  91. package/dist/services/customer-group.d.ts +0 -9
  92. package/dist/services/customer.d.ts +0 -10
  93. package/dist/services/discount-code.d.ts +0 -9
  94. package/dist/services/extension.d.ts +0 -9
  95. package/dist/services/inventory-entry.d.ts +0 -9
  96. package/dist/services/my-cart.d.ts +0 -11
  97. package/dist/services/my-customer.d.ts +0 -13
  98. package/dist/services/my-order.d.ts +0 -10
  99. package/dist/services/my-payment.d.ts +0 -9
  100. package/dist/services/order.d.ts +0 -12
  101. package/dist/services/payment.d.ts +0 -9
  102. package/dist/services/product-projection.d.ts +0 -11
  103. package/dist/services/product-type.d.ts +0 -11
  104. package/dist/services/product.d.ts +0 -9
  105. package/dist/services/project.d.ts +0 -11
  106. package/dist/services/shipping-method.d.ts +0 -10
  107. package/dist/services/shopping-list.d.ts +0 -9
  108. package/dist/services/state.d.ts +0 -9
  109. package/dist/services/store.d.ts +0 -11
  110. package/dist/services/subscription.d.ts +0 -9
  111. package/dist/services/tax-category.d.ts +0 -11
  112. package/dist/services/type.d.ts +0 -9
  113. package/dist/services/zone.d.ts +0 -9
  114. package/dist/storage.d.ts +0 -56
  115. package/dist/types.d.ts +0 -89
  116. package/dist/validate.d.ts +0 -7482
  117. package/src/lib/filterParser.test.ts +0 -15
  118. package/src/lib/filterParser.ts +0 -17
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.6.5",
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
- "engines": {
11
- "node": ">=10"
10
+ "exports": {
11
+ ".": {
12
+ "require": "./dist/index.js",
13
+ "import": "./dist/index.mjs"
14
+ }
12
15
  },
13
- "scripts": {
14
- "start": "tsdx watch",
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-typescript": "^7.16.7",
33
- "@commercetools/platform-sdk": "^2.4.1",
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
- "tsdx": "^0.14.1",
52
+ "tsc": "^2.0.4",
46
53
  "tslib": "^2.3.1",
47
- "typescript": "^4.5.4"
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
+ }