@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
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import {
|
|
2
|
+
InvalidInputError,
|
|
3
|
+
Price,
|
|
4
|
+
Product,
|
|
5
|
+
ProductVariant,
|
|
6
|
+
} from '@commercetools/platform-sdk'
|
|
7
|
+
import { CommercetoolsError } from './exceptions'
|
|
8
|
+
import { Writable } from './types'
|
|
9
|
+
|
|
10
|
+
export type PriceSelector = {
|
|
11
|
+
currency?: string
|
|
12
|
+
country?: string
|
|
13
|
+
customerGroup?: string
|
|
14
|
+
channel?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Apply the price selector on all the variants. The price selector is applied
|
|
19
|
+
* on all the prices per variant and the first match per variant is stored in
|
|
20
|
+
* the scopedPrice attribute
|
|
21
|
+
*/
|
|
22
|
+
export const applyPriceSelector = (
|
|
23
|
+
products: Product[],
|
|
24
|
+
selector: PriceSelector
|
|
25
|
+
) => {
|
|
26
|
+
validatePriceSelector(selector)
|
|
27
|
+
|
|
28
|
+
for (const product of products) {
|
|
29
|
+
const variants: Writable<ProductVariant>[] = [
|
|
30
|
+
product.masterData.staged?.masterVariant,
|
|
31
|
+
...(product.masterData.staged?.variants || []),
|
|
32
|
+
|
|
33
|
+
product.masterData.current?.masterVariant,
|
|
34
|
+
...(product.masterData.current?.variants || []),
|
|
35
|
+
].filter(x => x != undefined)
|
|
36
|
+
|
|
37
|
+
for (const variant of variants) {
|
|
38
|
+
const scopedPrices =
|
|
39
|
+
variant.prices?.filter(p => priceSelectorFilter(p, selector)) ?? []
|
|
40
|
+
|
|
41
|
+
if (scopedPrices.length > 0) {
|
|
42
|
+
const price = scopedPrices[0]
|
|
43
|
+
|
|
44
|
+
variant.scopedPriceDiscounted = false
|
|
45
|
+
variant.scopedPrice = {
|
|
46
|
+
...price,
|
|
47
|
+
currentValue: price.value,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const validatePriceSelector = (selector: PriceSelector) => {
|
|
55
|
+
if (
|
|
56
|
+
(selector.country || selector.channel || selector.customerGroup) &&
|
|
57
|
+
!selector.currency
|
|
58
|
+
) {
|
|
59
|
+
throw new CommercetoolsError<InvalidInputError>(
|
|
60
|
+
{
|
|
61
|
+
code: 'InvalidInput',
|
|
62
|
+
message:
|
|
63
|
+
'The price selecting parameters country, channel and customerGroup ' +
|
|
64
|
+
'cannot be used without the currency.',
|
|
65
|
+
},
|
|
66
|
+
400
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Return a boolean to indicate if the price matches the selector. Price
|
|
73
|
+
* selection requires that if the selector or the price has a specific value
|
|
74
|
+
* then it should match.
|
|
75
|
+
*/
|
|
76
|
+
export const priceSelectorFilter = (
|
|
77
|
+
price: Price,
|
|
78
|
+
selector: PriceSelector
|
|
79
|
+
): boolean => {
|
|
80
|
+
if (
|
|
81
|
+
(selector.country || price.country) &&
|
|
82
|
+
selector.country !== price.country
|
|
83
|
+
) {
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (
|
|
88
|
+
(selector.currency || price.value.currencyCode) &&
|
|
89
|
+
selector.currency !== price.value.currencyCode
|
|
90
|
+
) {
|
|
91
|
+
return false
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (
|
|
95
|
+
(selector.channel || price.channel?.id) &&
|
|
96
|
+
selector.channel !== price.channel?.id
|
|
97
|
+
) {
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (
|
|
102
|
+
(selector.customerGroup || price.customerGroup?.id) &&
|
|
103
|
+
selector.customerGroup !== price.customerGroup?.id
|
|
104
|
+
) {
|
|
105
|
+
return false
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return true
|
|
109
|
+
}
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import {
|
|
2
|
+
InvalidInputError,
|
|
3
|
+
ProductProjectionPagedSearchResponse,
|
|
4
|
+
Product,
|
|
5
|
+
ProductProjection,
|
|
6
|
+
QueryParam,
|
|
7
|
+
FacetResults,
|
|
8
|
+
FacetTerm,
|
|
9
|
+
TermFacetResult,
|
|
10
|
+
RangeFacetResult,
|
|
11
|
+
FilteredFacetResult,
|
|
12
|
+
} from '@commercetools/platform-sdk'
|
|
13
|
+
import { ByProjectKeyProductProjectionsSearchRequestBuilder } from '@commercetools/platform-sdk/dist/declarations/src/generated/client/search/by-project-key-product-projections-search-request-builder'
|
|
14
|
+
import { nestedLookup } from './helpers'
|
|
15
|
+
import { ProductService } from './services/product'
|
|
16
|
+
import { Writable } from './types'
|
|
17
|
+
import { CommercetoolsError } from './exceptions'
|
|
18
|
+
import {
|
|
19
|
+
FilterExpression,
|
|
20
|
+
generateFacetFunc,
|
|
21
|
+
getVariants,
|
|
22
|
+
parseFilterExpression,
|
|
23
|
+
RangeExpression,
|
|
24
|
+
resolveVariantValue,
|
|
25
|
+
} from './lib/projectionSearchFilter'
|
|
26
|
+
import { applyPriceSelector } from './priceSelector'
|
|
27
|
+
import { AbstractStorage } from './storage'
|
|
28
|
+
|
|
29
|
+
export type ProductProjectionSearchParams = {
|
|
30
|
+
fuzzy?: boolean
|
|
31
|
+
fuzzyLevel?: number
|
|
32
|
+
markMatchingVariants?: boolean
|
|
33
|
+
staged?: boolean
|
|
34
|
+
filter?: string[]
|
|
35
|
+
'filter.facets'?: string[]
|
|
36
|
+
'filter.query'?: string[]
|
|
37
|
+
facet?: string | string[]
|
|
38
|
+
sort?: string | string[]
|
|
39
|
+
limit?: number
|
|
40
|
+
offset?: number
|
|
41
|
+
withTotal?: boolean
|
|
42
|
+
priceCurrency?: string
|
|
43
|
+
priceCountry?: string
|
|
44
|
+
priceCustomerGroup?: string
|
|
45
|
+
priceChannel?: string
|
|
46
|
+
localeProjection?: string
|
|
47
|
+
storeProjection?: string
|
|
48
|
+
expand?: string | string[]
|
|
49
|
+
[key: string]: QueryParam
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class ProductProjectionSearch {
|
|
53
|
+
protected _storage: AbstractStorage
|
|
54
|
+
|
|
55
|
+
constructor(storage: AbstractStorage) {
|
|
56
|
+
this._storage = storage
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
search(
|
|
60
|
+
projectKey: string,
|
|
61
|
+
params: ProductProjectionSearchParams
|
|
62
|
+
): ProductProjectionPagedSearchResponse {
|
|
63
|
+
// Get a copy of all the products in the storage engine. We need a copy
|
|
64
|
+
// since we will be modifying the data.
|
|
65
|
+
let resources = this._storage
|
|
66
|
+
.all(projectKey, 'product')
|
|
67
|
+
.map(r => JSON.parse(JSON.stringify(r)))
|
|
68
|
+
|
|
69
|
+
let markMatchingVariant = params.markMatchingVariants ?? false
|
|
70
|
+
|
|
71
|
+
// Apply the priceSelector
|
|
72
|
+
applyPriceSelector(resources, {
|
|
73
|
+
country: params.priceCountry,
|
|
74
|
+
channel: params.priceChannel,
|
|
75
|
+
customerGroup: params.priceCustomerGroup,
|
|
76
|
+
currency: params.priceCurrency,
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// Apply filters pre facetting
|
|
80
|
+
if (params.filter) {
|
|
81
|
+
try {
|
|
82
|
+
const filters = params.filter.map(f =>
|
|
83
|
+
parseFilterExpression(f, params.staged ?? false)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
// Filters can modify the output. So clone the resources first.
|
|
87
|
+
resources = resources.filter(resource =>
|
|
88
|
+
filters.every(f => f(resource, markMatchingVariant))
|
|
89
|
+
)
|
|
90
|
+
} catch (err) {
|
|
91
|
+
throw new CommercetoolsError<InvalidInputError>(
|
|
92
|
+
{
|
|
93
|
+
code: 'InvalidInput',
|
|
94
|
+
message: (err as any).message,
|
|
95
|
+
},
|
|
96
|
+
400
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// TODO: Calculate facets
|
|
102
|
+
const facets = this.getFacets(params, resources)
|
|
103
|
+
|
|
104
|
+
// Apply filters post facetting
|
|
105
|
+
if (params['filter.query']) {
|
|
106
|
+
try {
|
|
107
|
+
const filters = params['filter.query'].map(f =>
|
|
108
|
+
parseFilterExpression(f, params.staged ?? false)
|
|
109
|
+
)
|
|
110
|
+
resources = resources.filter(resource =>
|
|
111
|
+
filters.every(f => f(resource, markMatchingVariant))
|
|
112
|
+
)
|
|
113
|
+
} catch (err) {
|
|
114
|
+
throw new CommercetoolsError<InvalidInputError>(
|
|
115
|
+
{
|
|
116
|
+
code: 'InvalidInput',
|
|
117
|
+
message: (err as any).message,
|
|
118
|
+
},
|
|
119
|
+
400
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Get the total before slicing the array
|
|
125
|
+
const totalResources = resources.length
|
|
126
|
+
|
|
127
|
+
// Apply offset, limit
|
|
128
|
+
const offset = params.offset || 0
|
|
129
|
+
const limit = params.limit || 20
|
|
130
|
+
resources = resources.slice(offset, offset + limit)
|
|
131
|
+
|
|
132
|
+
// Expand the resources
|
|
133
|
+
if (params.expand !== undefined) {
|
|
134
|
+
resources = resources.map(resource => {
|
|
135
|
+
return this._storage.expand(projectKey, resource, params.expand)
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
count: totalResources,
|
|
141
|
+
total: resources.length,
|
|
142
|
+
offset: offset,
|
|
143
|
+
limit: limit,
|
|
144
|
+
results: resources.map(this.transform),
|
|
145
|
+
facets: facets,
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
transform(product: Product): ProductProjection {
|
|
150
|
+
const obj = product.masterData.current
|
|
151
|
+
return {
|
|
152
|
+
id: product.id,
|
|
153
|
+
createdAt: product.createdAt,
|
|
154
|
+
lastModifiedAt: product.lastModifiedAt,
|
|
155
|
+
version: product.version,
|
|
156
|
+
name: obj.name,
|
|
157
|
+
slug: obj.slug,
|
|
158
|
+
categories: obj.categories,
|
|
159
|
+
masterVariant: obj.masterVariant,
|
|
160
|
+
variants: obj.variants,
|
|
161
|
+
productType: product.productType,
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
getFacets(
|
|
166
|
+
params: ProductProjectionSearchParams,
|
|
167
|
+
products: Product[]
|
|
168
|
+
): FacetResults {
|
|
169
|
+
if (!params.facet) return {}
|
|
170
|
+
const staged = false
|
|
171
|
+
const result: FacetResults = {}
|
|
172
|
+
|
|
173
|
+
for (const facet of params.facet) {
|
|
174
|
+
const expression = generateFacetFunc(facet)
|
|
175
|
+
|
|
176
|
+
// Term Facet
|
|
177
|
+
if (expression.type === 'TermExpression') {
|
|
178
|
+
result[facet] = this.termFacet(expression.source, products, staged)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Range Facet
|
|
182
|
+
if (expression.type === 'RangeExpression') {
|
|
183
|
+
result[expression.source] = this.rangeFacet(
|
|
184
|
+
expression.source,
|
|
185
|
+
expression.children,
|
|
186
|
+
products,
|
|
187
|
+
staged
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// FilteredFacet
|
|
192
|
+
if (expression.type === 'FilterExpression') {
|
|
193
|
+
result[expression.source] = this.filterFacet(
|
|
194
|
+
expression.source,
|
|
195
|
+
expression.children,
|
|
196
|
+
products,
|
|
197
|
+
staged
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return result
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* TODO: This implemention needs the following additional features:
|
|
207
|
+
* - counting products
|
|
208
|
+
* - correct dataType
|
|
209
|
+
*/
|
|
210
|
+
termFacet(
|
|
211
|
+
facet: string,
|
|
212
|
+
products: Product[],
|
|
213
|
+
staged: boolean
|
|
214
|
+
): TermFacetResult {
|
|
215
|
+
const result: Writable<TermFacetResult> = {
|
|
216
|
+
type: 'terms',
|
|
217
|
+
dataType: 'text',
|
|
218
|
+
missing: 0,
|
|
219
|
+
total: 0,
|
|
220
|
+
other: 0,
|
|
221
|
+
terms: [],
|
|
222
|
+
}
|
|
223
|
+
const terms: Record<any, number> = {}
|
|
224
|
+
|
|
225
|
+
if (facet.startsWith('variants.')) {
|
|
226
|
+
products.forEach(p => {
|
|
227
|
+
const variants = getVariants(p, staged)
|
|
228
|
+
variants.forEach(v => {
|
|
229
|
+
result.total++
|
|
230
|
+
|
|
231
|
+
let value = resolveVariantValue(v, facet)
|
|
232
|
+
if (value === undefined) {
|
|
233
|
+
result.missing++
|
|
234
|
+
} else {
|
|
235
|
+
if (typeof value === 'number') {
|
|
236
|
+
value = Number(value).toFixed(1)
|
|
237
|
+
}
|
|
238
|
+
terms[value] = value in terms ? terms[value] + 1 : 1
|
|
239
|
+
}
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
} else {
|
|
243
|
+
products.forEach(p => {
|
|
244
|
+
const value = nestedLookup(p, facet)
|
|
245
|
+
result.total++
|
|
246
|
+
if (value === undefined) {
|
|
247
|
+
result.missing++
|
|
248
|
+
} else {
|
|
249
|
+
terms[value] = value in terms ? terms[value] + 1 : 1
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
for (const term in terms) {
|
|
254
|
+
result.terms.push({
|
|
255
|
+
term: term as any,
|
|
256
|
+
count: terms[term],
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
return result
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
filterFacet(
|
|
263
|
+
source: string,
|
|
264
|
+
filters: FilterExpression[] | undefined,
|
|
265
|
+
products: Product[],
|
|
266
|
+
staged: boolean
|
|
267
|
+
): FilteredFacetResult {
|
|
268
|
+
let count = 0
|
|
269
|
+
if (source.startsWith('variants.')) {
|
|
270
|
+
for (const p of products) {
|
|
271
|
+
for (const v of getVariants(p, staged)) {
|
|
272
|
+
const val = resolveVariantValue(v, source)
|
|
273
|
+
if (filters?.some(f => f.match(val))) {
|
|
274
|
+
count++
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
throw new Error('not supported')
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
type: 'filter',
|
|
284
|
+
count: count,
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
rangeFacet(
|
|
289
|
+
source: string,
|
|
290
|
+
ranges: RangeExpression[] | undefined,
|
|
291
|
+
products: Product[],
|
|
292
|
+
staged: boolean
|
|
293
|
+
): RangeFacetResult {
|
|
294
|
+
const counts =
|
|
295
|
+
ranges?.map(range => {
|
|
296
|
+
if (source.startsWith('variants.')) {
|
|
297
|
+
const values = []
|
|
298
|
+
for (const p of products) {
|
|
299
|
+
for (const v of getVariants(p, staged)) {
|
|
300
|
+
const val = resolveVariantValue(v, source)
|
|
301
|
+
if (val === undefined) {
|
|
302
|
+
continue
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (range.match(val)) {
|
|
306
|
+
values.push(val)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const numValues = values.length
|
|
312
|
+
return {
|
|
313
|
+
type: 'double',
|
|
314
|
+
from: range.start || 0,
|
|
315
|
+
fromStr: range.start !== null ? Number(range.start).toFixed(1) : '',
|
|
316
|
+
to: range.stop || 0,
|
|
317
|
+
toStr: range.stop !== null ? Number(range.stop).toFixed(1) : '',
|
|
318
|
+
count: numValues,
|
|
319
|
+
// totalCount: 0,
|
|
320
|
+
total: values.reduce((a, b) => a + b, 0),
|
|
321
|
+
min: numValues > 0 ? Math.min(...values) : 0,
|
|
322
|
+
max: numValues > 0 ? Math.max(...values) : 0,
|
|
323
|
+
mean: numValues > 0 ? mean(values) : 0,
|
|
324
|
+
}
|
|
325
|
+
} else {
|
|
326
|
+
throw new Error('not supported')
|
|
327
|
+
}
|
|
328
|
+
}) || []
|
|
329
|
+
const data: RangeFacetResult = {
|
|
330
|
+
type: 'range',
|
|
331
|
+
// @ts-ignore
|
|
332
|
+
dataType: 'number',
|
|
333
|
+
ranges: counts,
|
|
334
|
+
}
|
|
335
|
+
return data
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const mean = (arr: number[]) => {
|
|
340
|
+
let total = 0
|
|
341
|
+
for (let i = 0; i < arr.length; i++) {
|
|
342
|
+
total += arr[i]
|
|
343
|
+
}
|
|
344
|
+
return total / arr.length
|
|
345
|
+
}
|
package/src/projectAPI.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
+
import { ReferenceTypeId } from '@commercetools/platform-sdk'
|
|
1
2
|
import { GetParams } from 'repositories/abstract'
|
|
2
3
|
import { getBaseResourceProperties } from './helpers'
|
|
3
4
|
import { AbstractStorage } from './storage'
|
|
4
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
RepositoryMap,
|
|
7
|
+
RepositoryTypes,
|
|
8
|
+
ResourceMap,
|
|
9
|
+
Services,
|
|
10
|
+
ServiceTypes,
|
|
11
|
+
} from './types'
|
|
5
12
|
|
|
6
13
|
export class ProjectAPI {
|
|
7
14
|
private projectKey: string
|
|
@@ -18,18 +25,10 @@ export class ProjectAPI {
|
|
|
18
25
|
this._services = services
|
|
19
26
|
}
|
|
20
27
|
|
|
21
|
-
add
|
|
22
|
-
|
|
23
|
-
resource: ResourceMap[ReferenceTypeId]
|
|
24
|
-
) {
|
|
25
|
-
//@ts-ignore
|
|
26
|
-
if (typeId === 'custom-object') typeId = 'key-value-document'
|
|
27
|
-
|
|
28
|
-
const parsedTypeId = typeId as ReferenceTypeId
|
|
29
|
-
|
|
30
|
-
const service = this._services[parsedTypeId]
|
|
28
|
+
add(typeId: ReferenceTypeId, resource: ResourceMap[ReferenceTypeId]) {
|
|
29
|
+
const service = this._services[typeId]
|
|
31
30
|
if (service) {
|
|
32
|
-
this._storage.add(this.projectKey,
|
|
31
|
+
this._storage.add(this.projectKey, typeId as ReferenceTypeId, {
|
|
33
32
|
...getBaseResourceProperties(),
|
|
34
33
|
...resource,
|
|
35
34
|
})
|
|
@@ -38,26 +37,26 @@ export class ProjectAPI {
|
|
|
38
37
|
}
|
|
39
38
|
}
|
|
40
39
|
|
|
41
|
-
get<
|
|
42
|
-
typeId:
|
|
40
|
+
get<RT extends RepositoryTypes>(
|
|
41
|
+
typeId: RT,
|
|
43
42
|
id: string,
|
|
44
43
|
params?: GetParams
|
|
45
|
-
): ResourceMap[
|
|
44
|
+
): ResourceMap[RT] {
|
|
46
45
|
return this._storage.get(
|
|
47
46
|
this.projectKey,
|
|
48
47
|
typeId,
|
|
49
48
|
id,
|
|
50
49
|
params
|
|
51
|
-
) as ResourceMap[
|
|
50
|
+
) as ResourceMap[RT]
|
|
52
51
|
}
|
|
53
52
|
|
|
54
53
|
// TODO: Not sure if we want to expose this...
|
|
55
|
-
getRepository<
|
|
56
|
-
typeId:
|
|
57
|
-
): RepositoryMap[
|
|
54
|
+
getRepository<RT extends keyof RepositoryMap>(
|
|
55
|
+
typeId: ServiceTypes
|
|
56
|
+
): RepositoryMap[RT] {
|
|
58
57
|
const service = this._services[typeId]
|
|
59
58
|
if (service !== undefined) {
|
|
60
|
-
return service.repository as RepositoryMap[
|
|
59
|
+
return service.repository as RepositoryMap[RT]
|
|
61
60
|
}
|
|
62
61
|
throw new Error('No such repository')
|
|
63
62
|
}
|
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
CategoryDraft,
|
|
7
7
|
CategorySetAssetDescriptionAction,
|
|
8
8
|
CategorySetAssetSourcesAction,
|
|
9
|
+
CategorySetCustomFieldAction,
|
|
10
|
+
CategorySetCustomTypeAction,
|
|
9
11
|
CategorySetDescriptionAction,
|
|
10
12
|
CategorySetKeyAction,
|
|
11
13
|
CategorySetMetaDescriptionAction,
|
|
@@ -51,6 +53,11 @@ export class CategoryRepository extends AbstractResourceRepository {
|
|
|
51
53
|
),
|
|
52
54
|
}
|
|
53
55
|
}) || [],
|
|
56
|
+
custom: createCustomFields(
|
|
57
|
+
draft.custom,
|
|
58
|
+
context.projectKey,
|
|
59
|
+
this._storage
|
|
60
|
+
),
|
|
54
61
|
}
|
|
55
62
|
this.save(context, resource)
|
|
56
63
|
return resource
|
|
@@ -141,5 +148,34 @@ export class CategoryRepository extends AbstractResourceRepository {
|
|
|
141
148
|
) => {
|
|
142
149
|
resource.metaTitle = metaTitle
|
|
143
150
|
},
|
|
151
|
+
setCustomType: (
|
|
152
|
+
context: RepositoryContext,
|
|
153
|
+
resource: Writable<Category>,
|
|
154
|
+
{ type, fields }: CategorySetCustomTypeAction
|
|
155
|
+
) => {
|
|
156
|
+
if (type) {
|
|
157
|
+
resource.custom = createCustomFields(
|
|
158
|
+
{ type, fields },
|
|
159
|
+
context.projectKey,
|
|
160
|
+
this._storage
|
|
161
|
+
)
|
|
162
|
+
} else {
|
|
163
|
+
resource.custom = undefined
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
setCustomField: (
|
|
167
|
+
context: RepositoryContext,
|
|
168
|
+
resource: Writable<Category>,
|
|
169
|
+
{ name, value }: CategorySetCustomFieldAction
|
|
170
|
+
) => {
|
|
171
|
+
if (!resource.custom) {
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
if (value === null) {
|
|
175
|
+
delete resource.custom.fields[name]
|
|
176
|
+
} else {
|
|
177
|
+
resource.custom.fields[name] = value
|
|
178
|
+
}
|
|
179
|
+
},
|
|
144
180
|
}
|
|
145
181
|
}
|