@labdigital/commercetools-mock 0.7.1 → 0.9.1

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 CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.7.1",
2
+ "version": "0.9.1",
3
3
  "license": "MIT",
4
4
  "main": "dist/index.js",
5
5
  "typings": "dist/index.d.ts",
@@ -7,6 +7,12 @@
7
7
  "dist",
8
8
  "src"
9
9
  ],
10
+ "exports": {
11
+ ".": {
12
+ "require": "./dist/index.js",
13
+ "import": "./dist/index.mjs"
14
+ }
15
+ },
10
16
  "engines": {
11
17
  "node": ">=14"
12
18
  },
@@ -50,8 +56,6 @@
50
56
  },
51
57
  "dependencies": {
52
58
  "@types/lodash": "^4.14.182",
53
- "ajv": "^8.8.2",
54
- "ajv-formats": "^2.1.1",
55
59
  "basic-auth": "^2.0.1",
56
60
  "body-parser": "^1.20.0",
57
61
  "deep-equal": "^2.0.5",
@@ -69,7 +73,6 @@
69
73
  },
70
74
  "scripts": {
71
75
  "start": "nodemon --watch src --exec 'node -r esbuild-register' src/server.ts",
72
- "generate": "node -r esbuild-register generate-validation.ts",
73
76
  "build": "tsup",
74
77
  "test": "jest test --coverage",
75
78
  "lint": "eslint"
package/src/helpers.ts CHANGED
@@ -11,6 +11,30 @@ export const getBaseResourceProperties = () => {
11
11
  }
12
12
  }
13
13
 
14
+ /**
15
+ * Do a nested lookup by using a path. For example `foo.bar.value` will
16
+ * return obj['foo']['bar']['value']
17
+ */
18
+ export const nestedLookup = (obj: any, path: string): any => {
19
+ if (!path || path === '') {
20
+ return obj
21
+ }
22
+
23
+ const parts = path.split('.')
24
+ let val = obj
25
+
26
+ for (let i = 0; i < parts.length; i++) {
27
+ const part = parts[i]
28
+ if (val == undefined) {
29
+ return undefined
30
+ }
31
+
32
+ val = val[part]
33
+ }
34
+
35
+ return val
36
+ }
37
+
14
38
  export const QueryParamsAsArray = (
15
39
  input: string | ParsedQs | string[] | ParsedQs[] | undefined
16
40
  ): string[] => {
@@ -8,10 +8,12 @@ describe('Predicate filter', () => {
8
8
  arrayProperty: ['foo', 'bar', 'nar'],
9
9
  notDefined: undefined,
10
10
  emptyArrayProperty: [],
11
+ booleanProperty: true,
11
12
  nested: {
12
13
  numberProperty: 1234,
13
14
  objectProperty: {
14
15
  stringProperty: 'foobar',
16
+ booleanProperty: true,
15
17
  },
16
18
  },
17
19
 
@@ -31,6 +33,13 @@ describe('Predicate filter', () => {
31
33
  expect(match(`stringProperty=:val`, { val: 'foobar' })).toBeTruthy()
32
34
  })
33
35
 
36
+ test('booleanProperty = true', async () => {
37
+ expect(match(`booleanProperty != true`)).toBeFalsy()
38
+ expect(match(`booleanProperty = true`)).toBeTruthy()
39
+
40
+ expect(match(`booleanProperty=:val`, { val: true })).toBeTruthy()
41
+ })
42
+
34
43
  test('stringProperty matches ignore case "foobar"', async () => {
35
44
  expect(match(`stringProperty="FOObar"`)).toBeFalsy()
36
45
  expect(match(`stringProperty matches ignore case "FOObar"`)).toBeTruthy()
@@ -168,6 +177,13 @@ describe('Predicate filter', () => {
168
177
  ).toBeTruthy()
169
178
  })
170
179
 
180
+ test('nested attribute access', async () => {
181
+ expect(match(`nested(objectProperty(booleanProperty != true))`)).toBeFalsy()
182
+ expect(
183
+ match(`nested(objectProperty(booleanProperty != false))`)
184
+ ).toBeTruthy()
185
+ })
186
+
171
187
  test('lexer confusion', async () => {
172
188
  expect(() => match(`orSomething="foobar"`)).toThrow(PredicateError)
173
189
  expect(() => match(`orSomething="foobar"`)).toThrow(
@@ -124,6 +124,7 @@ const getLexer = (value: string) => {
124
124
  .token('FLOAT', /\d+\.\d+/)
125
125
  .token('INT', /\d+/)
126
126
  .token('VARIABLE', /:([-_A-Za-z0-9]+)/)
127
+ .token('BOOLEAN', /(true|false)/)
127
128
  .token('IDENTIFIER', /[-_A-Za-z0-9]+/)
128
129
  .token('STRING', /"((?:\\.|[^"\\])*)"/)
129
130
  .token('STRING', /'((?:\\.|[^'\\])*)'/)
@@ -159,6 +160,13 @@ const generateMatchFunc = (predicate: string): MatchFunc => {
159
160
  pos: t.token.strpos(),
160
161
  } as Symbol
161
162
  })
163
+ .nud('BOOLEAN', 1, t => {
164
+ return {
165
+ type: 'boolean',
166
+ value: t.token.match === 'true' ? true : false,
167
+ pos: t.token.strpos(),
168
+ } as Symbol
169
+ })
162
170
  .nud('VARIABLE', 100, t => {
163
171
  return {
164
172
  type: 'var',
@@ -254,7 +262,6 @@ const generateMatchFunc = (predicate: string): MatchFunc => {
254
262
  .led('!=', 20, ({ left, bp }) => {
255
263
  const expr = parser.parse({ terminals: [bp - 1] })
256
264
  validateSymbol(expr)
257
-
258
265
  return (obj: any, vars: VariableMap) => {
259
266
  return resolveValue(obj, left) !== resolveSymbol(expr, vars)
260
267
  }
@@ -127,6 +127,12 @@ describe('Search filter', () => {
127
127
  ).toBeTruthy()
128
128
  })
129
129
 
130
+ test('by price range - or', async () => {
131
+ expect(
132
+ match(`variants.price.centAmount:range (2 TO 1500 ), (1500 TO 3000), (3000 TO 6000)`).isMatch
133
+ ).toBeTruthy()
134
+ })
135
+
130
136
  test('by scopedPrice range', async () => {
131
137
  let result
132
138
  let products: Product[]
@@ -5,6 +5,7 @@
5
5
  import { Product, ProductVariant } from '@commercetools/platform-sdk'
6
6
  import perplex from 'perplex'
7
7
  import Parser from 'pratt'
8
+ import { nestedLookup } from '../helpers'
8
9
  import { Writable } from '../types'
9
10
 
10
11
  type MatchFunc = (target: any) => boolean
@@ -14,6 +15,43 @@ type ProductFilter = (
14
15
  markMatchingVariants: boolean
15
16
  ) => boolean
16
17
 
18
+ type Symbol = {
19
+ type: 'Symbol'
20
+ kind: 'int' | 'string' | 'any'
21
+ value: any
22
+ }
23
+
24
+ type RangeExpressionSet = {
25
+ source: string
26
+ type: 'RangeExpression'
27
+ children?: RangeExpression[]
28
+ }
29
+
30
+ type FilterExpressionSet = {
31
+ source: string
32
+ type: 'FilterExpression'
33
+ children?: FilterExpression[]
34
+ }
35
+
36
+ type TermExpressionSet = {
37
+ source: string
38
+ type: 'TermExpression'
39
+ }
40
+
41
+ type ExpressionSet = RangeExpressionSet | FilterExpressionSet | TermExpressionSet
42
+
43
+ export type RangeExpression = {
44
+ type: 'RangeExpression'
45
+ start?: number
46
+ stop?: number
47
+ match: (obj: any) => boolean
48
+ }
49
+
50
+ export type FilterExpression = {
51
+ type: 'FilterExpression'
52
+ match: (obj: any) => boolean
53
+ }
54
+
17
55
  /**
18
56
  * Returns a function (ProductFilter).
19
57
  * NOTE: The filter can alter the resources in-place (FIXME)
@@ -53,7 +91,7 @@ const getLexer = (value: string) => {
53
91
  .token('WS', /\s+/, true) // skip
54
92
  }
55
93
 
56
- const generateMatchFunc = (filter: string): MatchFunc => {
94
+ const parseFilter = (filter: string): ExpressionSet => {
57
95
  const lexer = getLexer(filter)
58
96
  const parser = new Parser(lexer)
59
97
  .builder()
@@ -61,42 +99,81 @@ const generateMatchFunc = (filter: string): MatchFunc => {
61
99
  return t.token.match
62
100
  })
63
101
  .led(':', 100, ({ left, bp }) => {
64
- const expr = parser.parse({ terminals: [bp - 1] })
102
+ let parsed: any = parser.parse({ terminals: [bp - 1] })
103
+ let expressions: RangeExpression[] | FilterExpression[] | Symbol[]
104
+ expressions = !Array.isArray(parsed) ? [parsed] : parsed
65
105
 
66
- if (Array.isArray(expr)) {
67
- return (obj: any): boolean => {
68
- return expr.includes(obj)
69
- }
106
+ // Make sure we only have one type of expression (cannot mix)
107
+ const unique = new Set(expressions.map(expr => expr.type))
108
+ if (unique.size > 1) {
109
+ throw new Error('Invalid expression')
70
110
  }
71
- if (typeof expr === 'function') {
72
- return (obj: any): boolean => {
73
- return expr(obj)
111
+
112
+ // Convert plain symbols to a filter expression. For example
113
+ // variants.attribute.foobar:4 where 4 is a Symbol should result
114
+ // in a comparison
115
+ if (expressions.some(expr => expr.type == 'Symbol')) {
116
+ return {
117
+ source: left as string,
118
+ type: 'FilterExpression',
119
+ children: expressions.map((e): FilterExpression => {
120
+ if (e.type != 'Symbol') {
121
+ throw new Error('Invalid expression')
122
+ }
123
+
124
+ return {
125
+ type: 'FilterExpression',
126
+ match: (obj: any): boolean => {
127
+ return obj === e.value
128
+ },
129
+ }
130
+ })
74
131
  }
75
132
  }
76
- return (obj: any): boolean => {
77
- return obj === expr
133
+
134
+ return {
135
+ source: left,
136
+ type: expressions[0].type,
137
+ children: expressions,
78
138
  }
79
139
  })
80
140
  .nud('STRING', 20, t => {
81
- // @ts-ignore
82
- return t.token.groups[1]
141
+ return {
142
+ type: 'Symbol',
143
+ kind: 'string',
144
+ // @ts-ignore
145
+ value: t.token.groups[1],
146
+ } as Symbol
83
147
  })
84
148
  .nud('INT', 5, t => {
85
- // @ts-ignore
86
- return parseInt(t.token.match, 10)
149
+ return {
150
+ type: 'Symbol',
151
+ kind: 'int',
152
+ value: parseInt(t.token.match, 10),
153
+ } as Symbol
87
154
  })
88
155
  .nud('STAR', 5, t => {
89
- return null
156
+ return {
157
+ type: 'Symbol',
158
+ kind: 'any',
159
+ value: null,
160
+ }
90
161
  })
91
162
  .nud('EXISTS', 10, ({ bp }) => {
92
- return (val: any) => {
93
- return val !== undefined
94
- }
163
+ return {
164
+ type: 'FilterExpression',
165
+ match: (obj: any): boolean => {
166
+ return obj !== undefined
167
+ },
168
+ } as FilterExpression
95
169
  })
96
170
  .nud('MISSING', 10, ({ bp }) => {
97
- return (val: any) => {
98
- return val === undefined
99
- }
171
+ return {
172
+ type: 'FilterExpression',
173
+ match: (obj: any): boolean => {
174
+ return obj === undefined
175
+ },
176
+ } as FilterExpression
100
177
  })
101
178
  .led('COMMA', 200, ({ left, token, bp }) => {
102
179
  const expr: any = parser.parse({ terminals: [bp - 1] })
@@ -106,52 +183,92 @@ const generateMatchFunc = (filter: string): MatchFunc => {
106
183
  return [left, expr]
107
184
  }
108
185
  })
186
+ .nud('(', 100, t => {
187
+ const expr: any = parser.parse({ terminals: [')'] })
188
+ lexer.expect(')')
189
+ return expr
190
+ })
109
191
  .bp(')', 0)
110
192
  .led('TO', 20, ({ left, bp }) => {
111
193
  const expr: any = parser.parse({ terminals: [bp - 1] })
112
- return [left, expr]
194
+ return {
195
+ start: left.value,
196
+ stop: expr.value,
197
+ }
113
198
  })
114
199
  .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
- }
200
+ let ranges: any = parser.parse()
201
+
202
+ // If multiple ranges are defined we receive an array of ranges. So let's
203
+ // make sure we always have an array
204
+ if (!Array.isArray(ranges)) {
205
+ ranges = [ranges]
127
206
  }
128
- 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
207
+
208
+ // Return a list of functions which matches the ranges. These functions
209
+ // are processed as an OR clause
210
+ return ranges.map((range: any) => {
211
+ let func = undefined
212
+
213
+ if (range.start !== null && range.stop !== null) {
214
+ func = (obj: any): boolean => {
215
+ return obj >= range.start && obj <= range.stop
216
+ }
217
+ } else if (range.start === null && range.stop !== null) {
218
+ func = (obj: any): boolean => {
219
+ return obj <= range.stop
220
+ }
221
+ } else if (range.start !== null && range.stop === null) {
222
+ func = (obj: any): boolean => {
223
+ return obj >= range.start
224
+ }
225
+ } else {
226
+ func = (obj: any): boolean => {
227
+ return true
228
+ }
135
229
  }
136
230
 
137
- }
231
+ return {
232
+ type: 'RangeExpression',
233
+ start: range.start,
234
+ stop: range.stop,
235
+ match: func,
236
+ } as RangeExpression
237
+ })
138
238
  })
139
239
  .build()
140
240
 
141
- const result = parser.parse()
241
+ return parser.parse()
242
+ }
142
243
 
143
- if (typeof result !== 'function') {
244
+ const generateMatchFunc = (filter: string) => {
245
+ const result = parseFilter(filter)
246
+ if (!result) {
144
247
  const lines = filter.split('\n')
145
248
  const column = lines[lines.length - 1].length
146
249
  throw new Error(`Syntax error while parsing '${filter}'.`)
147
250
  }
148
- return result
251
+ if (result.type == 'TermExpression') {
252
+ throw new Error(`Syntax error while parsing '${filter}'.`)
253
+ }
254
+
255
+ return (obj: any) => {
256
+ if (!result.children) return false
257
+ return result.children.some(c => c.match(obj))
258
+ }
149
259
  }
150
260
 
151
- const filterProduct = (
152
- source: string,
153
- exprFunc: MatchFunc
154
- ): ProductFilter => {
261
+ export const generateFacetFunc = (filter: string): ExpressionSet => {
262
+ if (!filter.includes(':')) {
263
+ return {
264
+ source: filter,
265
+ type: 'TermExpression',
266
+ }
267
+ }
268
+ return parseFilter(filter)
269
+ }
270
+
271
+ const filterProduct = (source: string, exprFunc: MatchFunc): ProductFilter => {
155
272
  return (p: Product, markMatchingVariants: boolean): boolean => {
156
273
  const value = nestedLookup(p, source)
157
274
  return exprFunc(value)
@@ -172,13 +289,12 @@ const filterVariants = (
172
289
  const value = resolveVariantValue(variant, path)
173
290
 
174
291
  if (exprFunc(value)) {
175
-
176
292
  // If markMatchingVariants parameter is true those ProductVariants that
177
293
  // match the search query have the additional field isMatchingVariant
178
294
  // set to true. For the other variants in the same product projection
179
295
  // this field is set to false.
180
296
  if (markMatchingVariants) {
181
- variants.forEach(v => v.isMatchingVariant = false)
297
+ variants.forEach(v => (v.isMatchingVariant = false))
182
298
  variant.isMatchingVariant = true
183
299
  }
184
300
  return true
@@ -189,10 +305,13 @@ const filterVariants = (
189
305
  }
190
306
  }
191
307
 
192
- const resolveVariantValue = (obj: ProductVariant, path: string): any => {
308
+ export const resolveVariantValue = (obj: ProductVariant, path: string): any => {
193
309
  if (path === undefined) {
194
310
  return obj
195
311
  }
312
+ if (path.startsWith('variants.')) {
313
+ path = path.substring(path.indexOf('.') + 1)
314
+ }
196
315
 
197
316
  if (path.startsWith('attributes.')) {
198
317
  const [, attrName, ...rest] = path.split('.')
@@ -216,27 +335,7 @@ const resolveVariantValue = (obj: ProductVariant, path: string): any => {
216
335
  return nestedLookup(obj, path)
217
336
  }
218
337
 
219
- const 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[] => {
338
+ export const getVariants = (p: Product, staged: boolean): ProductVariant[] => {
240
339
  return [
241
340
  staged
242
341
  ? p.masterData.staged?.masterVariant