@labdigital/commercetools-mock 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@labdigital/commercetools-mock",
3
3
  "author": "Michael van Tellingen",
4
- "version": "1.8.0",
4
+ "version": "1.9.0",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.cjs",
7
7
  "module": "dist/index.js",
package/src/helpers.ts CHANGED
@@ -32,17 +32,30 @@ export const nestedLookup = (obj: any, path: string): any => {
32
32
  return val
33
33
  }
34
34
 
35
- export const QueryParamsAsArray = (
35
+ export const queryParamsArray = (
36
36
  input: string | ParsedQs | string[] | ParsedQs[] | undefined
37
- ): string[] => {
37
+ ): string[] | undefined => {
38
38
  if (input == undefined) {
39
- return []
39
+ return undefined
40
40
  }
41
41
 
42
- if (Array.isArray(input)) {
43
- return input as string[]
42
+ const values: string[] = Array.isArray(input)
43
+ ? (input as string[])
44
+ : ([input] as string[])
45
+ if (values.length < 1) {
46
+ return undefined
44
47
  }
45
- return [input] as string[]
48
+ return values
49
+ }
50
+
51
+ export const queryParamsValue = (
52
+ value: string | ParsedQs | string[] | ParsedQs[] | undefined
53
+ ): string | undefined => {
54
+ const values = queryParamsArray(value)
55
+ if (values && values.length > 0) {
56
+ return values[0]
57
+ }
58
+ return undefined
46
59
  }
47
60
 
48
61
  export const cloneObject = <T>(o: T): T => JSON.parse(JSON.stringify(o))
@@ -171,15 +171,25 @@ export class ProductProjectionSearch {
171
171
  products: ProductProjection[]
172
172
  ): FacetResults {
173
173
  if (!params.facet) return {}
174
- const staged = false
175
174
  const result: FacetResults = {}
176
175
 
177
- for (const facet of params.facet) {
176
+ const regexp = new RegExp(/ counting products$/)
177
+ for (let facet of params.facet) {
178
+ let countProducts = false
179
+ if (facet.endsWith(' counting products')) {
180
+ facet = facet.replace(regexp, '')
181
+ countProducts = true
182
+ }
183
+
178
184
  const expression = generateFacetFunc(facet)
179
185
 
180
186
  // Term Facet
181
187
  if (expression.type === 'TermExpression') {
182
- result[facet] = this.termFacet(expression.source, products)
188
+ result[facet] = this.termFacet(
189
+ expression.source,
190
+ products,
191
+ countProducts
192
+ )
183
193
  }
184
194
 
185
195
  // Range Facet
@@ -187,7 +197,8 @@ export class ProductProjectionSearch {
187
197
  result[expression.source] = this.rangeFacet(
188
198
  expression.source,
189
199
  expression.children,
190
- products
200
+ products,
201
+ countProducts
191
202
  )
192
203
  }
193
204
 
@@ -196,7 +207,8 @@ export class ProductProjectionSearch {
196
207
  result[expression.source] = this.filterFacet(
197
208
  expression.source,
198
209
  expression.children,
199
- products
210
+ products,
211
+ countProducts
200
212
  )
201
213
  }
202
214
  }
@@ -209,7 +221,11 @@ export class ProductProjectionSearch {
209
221
  * - counting products
210
222
  * - correct dataType
211
223
  */
212
- termFacet(facet: string, products: ProductProjection[]): TermFacetResult {
224
+ termFacet(
225
+ facet: string,
226
+ products: ProductProjection[],
227
+ countProducts: boolean
228
+ ): TermFacetResult {
213
229
  const result: Writable<TermFacetResult> = {
214
230
  type: 'terms',
215
231
  dataType: 'text',
@@ -254,13 +270,15 @@ export class ProductProjectionSearch {
254
270
  count: terms[term],
255
271
  })
256
272
  }
273
+
257
274
  return result
258
275
  }
259
276
 
260
277
  filterFacet(
261
278
  source: string,
262
279
  filters: FilterExpression[] | undefined,
263
- products: ProductProjection[]
280
+ products: ProductProjection[],
281
+ countProducts: boolean
264
282
  ): FilteredFacetResult {
265
283
  let count = 0
266
284
  if (source.startsWith('variants.')) {
@@ -285,7 +303,8 @@ export class ProductProjectionSearch {
285
303
  rangeFacet(
286
304
  source: string,
287
305
  ranges: RangeExpression[] | undefined,
288
- products: ProductProjection[]
306
+ products: ProductProjection[],
307
+ countProducts: boolean
289
308
  ): RangeFacetResult {
290
309
  const counts =
291
310
  ranges?.map((range) => {
@@ -1,14 +1,215 @@
1
- import type { BusinessUnit } from '@commercetools/platform-sdk'
1
+ import {
2
+ type Associate,
3
+ type BusinessUnit,
4
+ type BusinessUnitAddAddressAction,
5
+ type BusinessUnitAddAssociateAction,
6
+ type BusinessUnitAddStoreAction,
7
+ type BusinessUnitChangeAddressAction,
8
+ type BusinessUnitChangeNameAction,
9
+ type BusinessUnitChangeParentUnitAction,
10
+ type BusinessUnitDraft,
11
+ type BusinessUnitSetAssociatesAction,
12
+ type BusinessUnitSetContactEmailAction,
13
+ type BusinessUnitSetStoreModeAction,
14
+ type Company,
15
+ type Division,
16
+ BusinessUnitChangeStatusAction,
17
+ CompanyDraft,
18
+ DivisionDraft,
19
+ } from '@commercetools/platform-sdk'
2
20
  import {
3
21
  AbstractResourceRepository,
4
22
  type RepositoryContext,
5
23
  } from './abstract.js'
24
+ import { getBaseResourceProperties } from '../helpers.js'
25
+ import {
26
+ createAddress,
27
+ createAssociate,
28
+ createCustomFields,
29
+ getBusinessUnitKeyReference,
30
+ getStoreKeyReference,
31
+ } from './helpers.js'
32
+ import { Writable } from '../types.js'
6
33
 
7
34
  export class BusinessUnitRepository extends AbstractResourceRepository<'business-unit'> {
8
35
  getTypeId() {
9
36
  return 'business-unit' as const
10
37
  }
11
- create(context: RepositoryContext, draft: any): BusinessUnit {
12
- throw new Error('Method not implemented.')
38
+
39
+ private _isCompanyDraft(draft: BusinessUnitDraft): draft is CompanyDraft {
40
+ return draft.unitType === 'Company'
41
+ }
42
+
43
+ private _isDivisionDraft(draft: BusinessUnitDraft): draft is DivisionDraft {
44
+ return draft.unitType === 'Division'
45
+ }
46
+
47
+ create(context: RepositoryContext, draft: BusinessUnitDraft): BusinessUnit {
48
+ const resource = {
49
+ ...getBaseResourceProperties(),
50
+ key: draft.key,
51
+ status: draft.status,
52
+ stores: draft.stores?.map((s) =>
53
+ getStoreKeyReference(s, context.projectKey, this._storage)
54
+ ),
55
+ storeMode: draft.storeMode,
56
+ name: draft.name,
57
+ contactEmail: draft.contactEmail,
58
+ addresses: draft.addresses?.map((a) =>
59
+ createAddress(a, context.projectKey, this._storage)
60
+ ),
61
+ custom: createCustomFields(
62
+ draft.custom,
63
+ context.projectKey,
64
+ this._storage
65
+ ),
66
+ shippingAddressIds: draft.shippingAddresses,
67
+ defaultShippingAddressId: draft.defaultShippingAddress,
68
+ billingAddressIds: draft.billingAddresses,
69
+ associateMode: draft.associateMode,
70
+ associates: draft.associates?.map((a) =>
71
+ createAssociate(a, context.projectKey, this._storage)
72
+ ),
73
+ }
74
+
75
+ if (this._isDivisionDraft(draft)) {
76
+ const division = {
77
+ ...resource,
78
+ parentUnit: getBusinessUnitKeyReference(
79
+ draft.parentUnit,
80
+ context.projectKey,
81
+ this._storage
82
+ ),
83
+ } as Division
84
+
85
+ this.saveNew(context, division)
86
+ return division
87
+ } else if (this._isCompanyDraft(draft)) {
88
+ const company = resource as Company
89
+
90
+ this.saveNew(context, company)
91
+ return company
92
+ }
93
+
94
+ throw new Error('Invalid business unit type')
95
+ }
96
+
97
+ actions = {
98
+ addAddress: (
99
+ context: RepositoryContext,
100
+ resource: Writable<BusinessUnit>,
101
+ { address }: BusinessUnitAddAddressAction
102
+ ) => {
103
+ const newAddress = createAddress(
104
+ address,
105
+ context.projectKey,
106
+ this._storage
107
+ )
108
+ if (newAddress) {
109
+ resource.addresses.push(newAddress)
110
+ }
111
+ },
112
+ addAssociate: (
113
+ context: RepositoryContext,
114
+ resource: Writable<BusinessUnit>,
115
+ { associate }: BusinessUnitAddAssociateAction
116
+ ) => {
117
+ const newAssociate = createAssociate(
118
+ associate,
119
+ context.projectKey,
120
+ this._storage
121
+ )
122
+ if (newAssociate) {
123
+ resource.associates.push(newAssociate)
124
+ }
125
+ },
126
+ setAssociates: (
127
+ context: RepositoryContext,
128
+ resource: Writable<BusinessUnit>,
129
+ { associates }: BusinessUnitSetAssociatesAction
130
+ ) => {
131
+ const newAssociates = associates
132
+ .map((a) => createAssociate(a, context.projectKey, this._storage))
133
+ .filter((a): a is Writable<Associate> => a !== undefined)
134
+ resource.associates = newAssociates || undefined
135
+ },
136
+ setContactEmail: (
137
+ context: RepositoryContext,
138
+ resource: Writable<BusinessUnit>,
139
+ { contactEmail }: BusinessUnitSetContactEmailAction
140
+ ) => {
141
+ resource.contactEmail = contactEmail
142
+ },
143
+ setStoreMode: (
144
+ context: RepositoryContext,
145
+ resource: Writable<BusinessUnit>,
146
+ { storeMode }: BusinessUnitSetStoreModeAction
147
+ ) => {
148
+ resource.storeMode = storeMode
149
+ },
150
+ changeAssociateMode: (
151
+ context: RepositoryContext,
152
+ resource: Writable<BusinessUnit>,
153
+ { storeMode }: BusinessUnitSetStoreModeAction
154
+ ) => {
155
+ resource.associateMode = storeMode
156
+ },
157
+ changeName: (
158
+ context: RepositoryContext,
159
+ resource: Writable<BusinessUnit>,
160
+ { name }: BusinessUnitChangeNameAction
161
+ ) => {
162
+ resource.name = name
163
+ },
164
+ changeAddress: (
165
+ context: RepositoryContext,
166
+ resource: Writable<BusinessUnit>,
167
+ { address }: BusinessUnitChangeAddressAction
168
+ ) => {
169
+ const newAddress = createAddress(
170
+ address,
171
+ context.projectKey,
172
+ this._storage
173
+ )
174
+ if (newAddress) {
175
+ resource.addresses.push(newAddress)
176
+ }
177
+ },
178
+ addStore: (
179
+ context: RepositoryContext,
180
+ resource: Writable<BusinessUnit>,
181
+ { store }: BusinessUnitAddStoreAction
182
+ ) => {
183
+ const newStore = getStoreKeyReference(
184
+ store,
185
+ context.projectKey,
186
+ this._storage
187
+ )
188
+ if (newStore) {
189
+ if (!resource.stores) {
190
+ resource.stores = []
191
+ }
192
+
193
+ resource.stores.push(newStore)
194
+ }
195
+ },
196
+ changeParentUnit: (
197
+ context: RepositoryContext,
198
+ resource: Writable<BusinessUnit>,
199
+ { parentUnit }: BusinessUnitChangeParentUnitAction
200
+ ) => {
201
+ resource.parentUnit = getBusinessUnitKeyReference(
202
+ parentUnit,
203
+ context.projectKey,
204
+ this._storage
205
+ )
206
+ },
207
+ changeStatus: (
208
+ context: RepositoryContext,
209
+ resource: Writable<BusinessUnit>,
210
+ { status }: BusinessUnitChangeStatusAction
211
+ ) => {
212
+ resource.status = status
213
+ },
13
214
  }
14
215
  }
@@ -5,6 +5,7 @@ import type {
5
5
  CartDiscountChangeTargetAction,
6
6
  CartDiscountDraft,
7
7
  CartDiscountSetCustomFieldAction,
8
+ CartDiscountSetCustomTypeAction,
8
9
  CartDiscountSetDescriptionAction,
9
10
  CartDiscountSetKeyAction,
10
11
  CartDiscountSetValidFromAction,
@@ -162,7 +163,6 @@ export class CartDiscountRepository extends AbstractResourceRepository<'cart-dis
162
163
  ) => {
163
164
  resource.target = target
164
165
  },
165
-
166
166
  setCustomField: (
167
167
  context: RepositoryContext,
168
168
  resource: Writable<CartDiscount>,
@@ -190,5 +190,30 @@ export class CartDiscountRepository extends AbstractResourceRepository<'cart-dis
190
190
  resource.custom.fields[name] = value
191
191
  }
192
192
  },
193
+ setCustomType: (
194
+ context: RepositoryContext,
195
+ resource: Writable<CartDiscount>,
196
+ { type, fields }: CartDiscountSetCustomTypeAction
197
+ ) => {
198
+ if (!type) {
199
+ resource.custom = undefined
200
+ } else {
201
+ const resolvedType = this._storage.getByResourceIdentifier(
202
+ context.projectKey,
203
+ type
204
+ )
205
+ if (!resolvedType) {
206
+ throw new Error(`Type ${type} not found`)
207
+ }
208
+
209
+ resource.custom = {
210
+ type: {
211
+ typeId: 'type',
212
+ id: resolvedType.id,
213
+ },
214
+ fields: fields || {},
215
+ }
216
+ }
217
+ },
193
218
  }
194
219
  }
@@ -1,24 +1,34 @@
1
- import type {
2
- Address,
3
- BaseAddress,
4
- CentPrecisionMoney,
5
- CustomFields,
6
- CustomFieldsDraft,
7
- HighPrecisionMoney,
8
- HighPrecisionMoneyDraft,
9
- InvalidJsonInputError,
10
- Price,
11
- PriceDraft,
12
- Reference,
13
- ReferencedResourceNotFoundError,
14
- ResourceIdentifier,
15
- Store,
16
- StoreKeyReference,
17
- StoreReference,
18
- StoreResourceIdentifier,
19
- Type,
20
- TypedMoney,
21
- _Money,
1
+ import {
2
+ AssociateRoleReference,
3
+ type Address,
4
+ type Associate,
5
+ type AssociateDraft,
6
+ type AssociateRoleAssignment,
7
+ type AssociateRoleAssignmentDraft,
8
+ type AssociateRoleKeyReference,
9
+ type AssociateRoleResourceIdentifier,
10
+ type BaseAddress,
11
+ type CentPrecisionMoney,
12
+ type CustomFields,
13
+ type CustomFieldsDraft,
14
+ type HighPrecisionMoney,
15
+ type HighPrecisionMoneyDraft,
16
+ type InvalidJsonInputError,
17
+ type Price,
18
+ type PriceDraft,
19
+ type Reference,
20
+ type ReferencedResourceNotFoundError,
21
+ type ResourceIdentifier,
22
+ type Store,
23
+ type StoreKeyReference,
24
+ type StoreReference,
25
+ type StoreResourceIdentifier,
26
+ type Type,
27
+ type TypedMoney,
28
+ type _Money,
29
+ BusinessUnitResourceIdentifier,
30
+ BusinessUnitKeyReference,
31
+ BusinessUnitReference,
22
32
  } from '@commercetools/platform-sdk'
23
33
  import type { Request } from 'express'
24
34
  import { v4 as uuidv4 } from 'uuid'
@@ -219,3 +229,90 @@ export const getRepositoryContext = (request: Request): RepositoryContext => ({
219
229
  projectKey: request.params.projectKey,
220
230
  storeKey: request.params.storeKey,
221
231
  })
232
+
233
+ export const createAssociate = (
234
+ a: AssociateDraft,
235
+ projectKey: string,
236
+ storage: AbstractStorage
237
+ ): Associate | undefined => {
238
+ if (!a) return undefined
239
+
240
+ if (!a.associateRoleAssignments) {
241
+ throw new Error('AssociateRoleAssignments is required')
242
+ }
243
+
244
+ return {
245
+ customer: getReferenceFromResourceIdentifier(
246
+ a.customer,
247
+ projectKey,
248
+ storage
249
+ ),
250
+ associateRoleAssignments: a.associateRoleAssignments?.map(
251
+ (a: AssociateRoleAssignmentDraft): AssociateRoleAssignment => ({
252
+ associateRole: getAssociateRoleKeyReference(
253
+ a.associateRole,
254
+ projectKey,
255
+ storage
256
+ ),
257
+ inheritance: a.inheritance as string,
258
+ })
259
+ ),
260
+ roles: a.roles as string[],
261
+ }
262
+ }
263
+
264
+ export const getAssociateRoleKeyReference = (
265
+ id: AssociateRoleResourceIdentifier,
266
+ projectKey: string,
267
+ storage: AbstractStorage
268
+ ): AssociateRoleKeyReference => {
269
+ if (id.key) {
270
+ return {
271
+ typeId: 'associate-role',
272
+ key: id.key,
273
+ }
274
+ }
275
+
276
+ const value = getReferenceFromResourceIdentifier<AssociateRoleReference>(
277
+ id,
278
+ projectKey,
279
+ storage
280
+ )
281
+
282
+ if (!value.obj?.key) {
283
+ throw new Error('No associate-role found for reference')
284
+ }
285
+
286
+ return {
287
+ typeId: 'associate-role',
288
+ key: value.obj?.key,
289
+ }
290
+ }
291
+
292
+ export const getBusinessUnitKeyReference = (
293
+ id: BusinessUnitResourceIdentifier,
294
+ projectKey: string,
295
+ storage: AbstractStorage
296
+ ): BusinessUnitKeyReference => {
297
+ if (id.key) {
298
+ return {
299
+ typeId: 'business-unit',
300
+ key: id.key,
301
+ }
302
+ }
303
+
304
+ const value = getReferenceFromResourceIdentifier<BusinessUnitReference>(
305
+ id,
306
+ projectKey,
307
+ storage
308
+ )
309
+
310
+ if (!value.obj?.key) {
311
+ throw new Error('No business-unit found for reference')
312
+ }
313
+
314
+ return {
315
+ typeId: 'business-unit',
316
+ key: value.obj?.key,
317
+ }
318
+ }
@@ -4,9 +4,7 @@ import type {
4
4
  ProductProjection,
5
5
  QueryParam,
6
6
  } from '@commercetools/platform-sdk'
7
- import { ParsedQs } from 'qs'
8
7
  import { CommercetoolsError } from '../exceptions.js'
9
- import { QueryParamsAsArray } from '../helpers.js'
10
8
  import { parseQueryExpression } from '../lib/predicateParser.js'
11
9
  import { ProductProjectionSearch } from '../product-projection-search.js'
12
10
  import { type AbstractStorage } from '../storage/index.js'
@@ -16,13 +14,13 @@ import {
16
14
  RepositoryContext,
17
15
  } from './abstract.js'
18
16
 
19
- type ProductProjectionQueryParams = {
17
+ export type ProductProjectionQueryParams = {
20
18
  staged?: boolean
21
19
  priceCurrency?: string
22
20
  priceCountry?: string
23
21
  priceCustomerGroup?: string
24
22
  priceChannel?: string
25
- localeProjection?: string | string[]
23
+ localeProjection?: string
26
24
  storeProjection?: string
27
25
  expand?: string | string[]
28
26
  sort?: string | string[]
@@ -126,18 +124,8 @@ export class ProductProjectionRepository extends AbstractResourceRepository<'pro
126
124
  }
127
125
  }
128
126
 
129
- search(context: RepositoryContext, query: ParsedQs) {
130
- const results = this._searchService.search(context.projectKey, {
131
- filter: QueryParamsAsArray(query.filter),
132
- 'filter.query': QueryParamsAsArray(query['filter.query']),
133
- facet: QueryParamsAsArray(query.facet),
134
- offset: query.offset ? Number(query.offset) : undefined,
135
- limit: query.limit ? Number(query.limit) : undefined,
136
- expand: QueryParamsAsArray(query.expand),
137
- staged: query.staged === 'true',
138
- })
139
-
140
- return results
127
+ search(context: RepositoryContext, query: ProductProjectionQueryParams) {
128
+ return this._searchService.search(context.projectKey, query)
141
129
  }
142
130
 
143
131
  actions = {}
@@ -3,6 +3,7 @@ import { type Request, type Response, Router } from 'express'
3
3
  import { ParsedQs } from 'qs'
4
4
  import { AbstractResourceRepository } from '../repositories/abstract.js'
5
5
  import { getRepositoryContext } from '../repositories/helpers.js'
6
+ import { queryParamsArray } from '../helpers.js'
6
7
 
7
8
  export default abstract class AbstractService {
8
9
  protected abstract getBasePath(): string
@@ -170,12 +171,6 @@ export default abstract class AbstractService {
170
171
  protected _parseParam(
171
172
  value: string | ParsedQs | string[] | ParsedQs[] | undefined
172
173
  ): string[] | undefined {
173
- if (Array.isArray(value)) {
174
- // @ts-ignore
175
- return value
176
- } else if (value !== undefined) {
177
- return [`${value}`]
178
- }
179
- return undefined
174
+ return queryParamsArray(value)
180
175
  }
181
176
  }
@@ -0,0 +1,42 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'vitest'
2
+ import { CommercetoolsMock } from '../ctMock.js'
3
+ import { BusinessUnit } from '@commercetools/platform-sdk'
4
+ import supertest from 'supertest'
5
+
6
+ describe('Business units query', () => {
7
+ const ctMock = new CommercetoolsMock()
8
+ let businessUnit: BusinessUnit | undefined
9
+
10
+ beforeEach(async () => {
11
+ const response = await supertest(ctMock.app)
12
+ .post('/dummy/business-units')
13
+ .send({
14
+ key: 'example-business-unit',
15
+ status: 'Active',
16
+ name: 'Example Business Unit',
17
+ unitType: 'Company',
18
+ })
19
+
20
+ expect(response.status).toBe(201)
21
+
22
+ businessUnit = response.body as BusinessUnit
23
+ })
24
+
25
+ afterEach(() => {
26
+ ctMock.clear()
27
+ })
28
+
29
+ test('no filter', async () => {
30
+ const response = await supertest(ctMock.app)
31
+ .get('/dummy/business-units')
32
+ .query('{}')
33
+ .send()
34
+
35
+ expect(response.status).toBe(200)
36
+ expect(response.body.count).toBe(1)
37
+
38
+ const businessUnit = response.body.results[0] as BusinessUnit
39
+
40
+ expect(businessUnit.key).toBe('example-business-unit')
41
+ })
42
+ })
@@ -0,0 +1,17 @@
1
+ import { Router } from 'express'
2
+ import { BusinessUnitRepository } from '../repositories/business-unit.js'
3
+ import AbstractService from './abstract.js'
4
+
5
+ export class BusinessUnitServices extends AbstractService {
6
+ public repository: BusinessUnitRepository
7
+
8
+ constructor(parent: Router, repository: BusinessUnitRepository) {
9
+ super(parent)
10
+
11
+ this.repository = repository
12
+ }
13
+
14
+ protected getBasePath(): string {
15
+ return 'business-units'
16
+ }
17
+ }
@@ -359,4 +359,22 @@ describe('Cart Discounts Update Actions', () => {
359
359
  })
360
360
  expect(response.status).toBe(400)
361
361
  })
362
+
363
+ test('remove all custom fields', async () => {
364
+ assert(cartDiscount, 'cart discount not created')
365
+
366
+ const response = await supertest(ctMock.app)
367
+ .post(`/dummy/cart-discounts/${cartDiscount.id}`)
368
+ .send({
369
+ version: 1,
370
+ actions: [
371
+ {
372
+ action: 'setCustomType',
373
+ },
374
+ ],
375
+ })
376
+ expect(response.status).toBe(200)
377
+ expect(response.body.version).toBe(2)
378
+ expect(response.body.custom).toBeUndefined()
379
+ })
362
380
  })