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