@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
@@ -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 { RepositoryMap, ResourceMap, Services } from './types'
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<ReferenceTypeId extends keyof ResourceMap>(
22
- typeId: ReferenceTypeId | 'custom-object',
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, parsedTypeId, {
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<ReferenceTypeId extends keyof ResourceMap>(
42
- typeId: ReferenceTypeId,
40
+ get<RT extends RepositoryTypes>(
41
+ typeId: RT,
43
42
  id: string,
44
43
  params?: GetParams
45
- ): ResourceMap[ReferenceTypeId] {
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[ReferenceTypeId]
50
+ ) as ResourceMap[RT]
52
51
  }
53
52
 
54
53
  // TODO: Not sure if we want to expose this...
55
- getRepository<ReferenceTypeId extends keyof RepositoryMap>(
56
- typeId: ReferenceTypeId
57
- ): RepositoryMap[ReferenceTypeId] {
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[ReferenceTypeId]
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
  }