@kontent-ai/delivery-sdk 16.4.3 → 16.4.5

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 (113) hide show
  1. package/.npmignore +15 -15
  2. package/LICENSE.md +9 -9
  3. package/dist/bundles/kontent-delivery.umd.js +5076 -4619
  4. package/dist/bundles/kontent-delivery.umd.js.map +1 -1
  5. package/dist/bundles/kontent-delivery.umd.min.js +1 -1
  6. package/dist/bundles/kontent-delivery.umd.min.js.map +1 -1
  7. package/dist/bundles/report.json +1 -1
  8. package/dist/bundles/report.min.json +1 -1
  9. package/dist/bundles/stats.json +7015 -5766
  10. package/dist/bundles/stats.min.json +11376 -10250
  11. package/dist/cjs/query/element/element-query.class.js +1 -1
  12. package/dist/cjs/query/element/element-query.class.js.map +1 -1
  13. package/dist/cjs/query/item/single-item-query.class.js +1 -1
  14. package/dist/cjs/query/item/single-item-query.class.js.map +1 -1
  15. package/dist/cjs/query/taxonomy/taxonomy-query.class.js +1 -1
  16. package/dist/cjs/query/taxonomy/taxonomy-query.class.js.map +1 -1
  17. package/dist/cjs/query/type/single-type-query.class.js +1 -1
  18. package/dist/cjs/query/type/single-type-query.class.js.map +1 -1
  19. package/dist/cjs/query/used-in/used-in-query.class.js +1 -1
  20. package/dist/cjs/query/used-in/used-in-query.class.js.map +1 -1
  21. package/dist/cjs/sdk-info.generated.js +1 -1
  22. package/dist/es6/query/element/element-query.class.js +1 -1
  23. package/dist/es6/query/element/element-query.class.js.map +1 -1
  24. package/dist/es6/query/item/single-item-query.class.js +1 -1
  25. package/dist/es6/query/item/single-item-query.class.js.map +1 -1
  26. package/dist/es6/query/taxonomy/taxonomy-query.class.js +1 -1
  27. package/dist/es6/query/taxonomy/taxonomy-query.class.js.map +1 -1
  28. package/dist/es6/query/type/single-type-query.class.js +1 -1
  29. package/dist/es6/query/type/single-type-query.class.js.map +1 -1
  30. package/dist/es6/query/used-in/used-in-query.class.js +1 -1
  31. package/dist/es6/query/used-in/used-in-query.class.js.map +1 -1
  32. package/dist/es6/sdk-info.generated.js +1 -1
  33. package/dist/esnext/query/element/element-query.class.js +1 -1
  34. package/dist/esnext/query/element/element-query.class.js.map +1 -1
  35. package/dist/esnext/query/item/single-item-query.class.js +1 -1
  36. package/dist/esnext/query/item/single-item-query.class.js.map +1 -1
  37. package/dist/esnext/query/taxonomy/taxonomy-query.class.js +1 -1
  38. package/dist/esnext/query/taxonomy/taxonomy-query.class.js.map +1 -1
  39. package/dist/esnext/query/type/single-type-query.class.js +1 -1
  40. package/dist/esnext/query/type/single-type-query.class.js.map +1 -1
  41. package/dist/esnext/query/used-in/used-in-query.class.js +1 -1
  42. package/dist/esnext/query/used-in/used-in-query.class.js.map +1 -1
  43. package/dist/esnext/sdk-info.generated.js +1 -1
  44. package/lib/client/delivery-client.factory.ts +9 -9
  45. package/lib/client/delivery-client.ts +163 -163
  46. package/lib/client/idelivery-client.interface.ts +110 -110
  47. package/lib/client/index.ts +3 -3
  48. package/lib/config/delivery-configs.ts +98 -98
  49. package/lib/config/index.ts +1 -1
  50. package/lib/contracts/contracts.ts +265 -265
  51. package/lib/contracts/index.ts +1 -1
  52. package/lib/elements/element-models.ts +89 -89
  53. package/lib/elements/element-resolver.ts +3 -3
  54. package/lib/elements/element-type.ts +16 -16
  55. package/lib/elements/elements.ts +70 -70
  56. package/lib/elements/index.ts +4 -4
  57. package/lib/images/image-url-transformation-builder.factory.ts +5 -5
  58. package/lib/images/image-url-transformation-builder.ts +181 -181
  59. package/lib/images/image.models.ts +4 -4
  60. package/lib/images/index.ts +3 -3
  61. package/lib/index.ts +12 -12
  62. package/lib/mappers/element.mapper.ts +504 -504
  63. package/lib/mappers/generic-element.mapper.ts +20 -20
  64. package/lib/mappers/index.ts +8 -8
  65. package/lib/mappers/item.mapper.ts +181 -181
  66. package/lib/mappers/language.mapper.ts +24 -24
  67. package/lib/mappers/sync.mapper.ts +32 -32
  68. package/lib/mappers/taxonomy.mapper.ts +77 -77
  69. package/lib/mappers/type.mapper.ts +76 -76
  70. package/lib/mappers/used-in.mapper.ts +20 -20
  71. package/lib/models/common/base-responses.ts +13 -13
  72. package/lib/models/common/common-models.ts +118 -118
  73. package/lib/models/common/filters.ts +280 -280
  74. package/lib/models/common/headers.ts +5 -5
  75. package/lib/models/common/index.ts +7 -7
  76. package/lib/models/common/pagination.class.ts +7 -7
  77. package/lib/models/common/parameters.ts +189 -189
  78. package/lib/models/common/sort-order.ts +1 -1
  79. package/lib/models/content-type-models.ts +42 -42
  80. package/lib/models/element-models.ts +51 -51
  81. package/lib/models/index.ts +9 -9
  82. package/lib/models/item-models.ts +192 -192
  83. package/lib/models/language-models.ts +17 -17
  84. package/lib/models/responses.ts +137 -137
  85. package/lib/models/sync-models.ts +18 -18
  86. package/lib/models/taxonomy-models.ts +25 -25
  87. package/lib/query/common/base-item-listing-query.class.ts +274 -274
  88. package/lib/query/common/base-listing-query.class.ts +95 -95
  89. package/lib/query/common/base-query.class.ts +148 -148
  90. package/lib/query/element/element-query.class.ts +47 -45
  91. package/lib/query/index.ts +13 -13
  92. package/lib/query/item/multiple-items-query.class.ts +183 -183
  93. package/lib/query/item/single-item-query.class.ts +106 -106
  94. package/lib/query/items-feed/items-feed-query.class.ts +191 -191
  95. package/lib/query/language/languages-query.class.ts +91 -91
  96. package/lib/query/sync/initialize-sync-query.class.ts +83 -83
  97. package/lib/query/sync/sync-changes-query.class.ts +56 -56
  98. package/lib/query/taxonomy/taxonomies-query.class.ts +82 -82
  99. package/lib/query/taxonomy/taxonomy-query.class.ts +50 -50
  100. package/lib/query/type/multiple-type-query.class.ts +95 -95
  101. package/lib/query/type/single-type-query.class.ts +45 -45
  102. package/lib/query/used-in/used-in-query.class.ts +128 -126
  103. package/lib/sdk-info.generated.ts +1 -1
  104. package/lib/services/base-delivery-query.service.ts +375 -375
  105. package/lib/services/delivery-query.service.ts +336 -336
  106. package/lib/services/index.ts +2 -2
  107. package/lib/services/mapping.service.ts +223 -223
  108. package/lib/utilities/codename.helper.ts +10 -10
  109. package/lib/utilities/delivery-url.helper.ts +11 -11
  110. package/lib/utilities/enum.helper.ts +38 -38
  111. package/lib/utilities/index.ts +3 -3
  112. package/package.json +96 -96
  113. package/readme.md +531 -531
@@ -1,504 +1,504 @@
1
- import { codenameHelper, deliveryUrlHelper, enumHelper } from '../utilities';
2
-
3
- import { IDeliveryClientConfig } from '../config';
4
- import { Contracts } from '../contracts';
5
- import { ElementModels, Elements, ElementType } from '../elements';
6
- import {
7
- IContentItem,
8
- IContentItemsContainer,
9
- IContentItemWithRawDataContainer,
10
- IContentItemWithRawElements,
11
- ILink,
12
- IMapElementsResult,
13
- IRichTextImage
14
- } from '../models';
15
-
16
- interface IRichTextImageUrlRecord {
17
- originalUrl: string;
18
- newUrl: string;
19
- }
20
-
21
- export class ElementMapper<TContentItemType extends IContentItem> {
22
- constructor(private readonly config: IDeliveryClientConfig) {}
23
-
24
- mapElements<TContentItem extends TContentItemType = TContentItemType>(data: {
25
- dataToMap: IContentItemWithRawElements;
26
- processedItems: IContentItemsContainer<TContentItem>;
27
- processingStartedForCodenames: string[];
28
- preparedItems: IContentItemWithRawDataContainer;
29
- }): IMapElementsResult<TContentItem, TContentItemType> | undefined {
30
- // return processed item to avoid infinite recursion
31
- const processedItem =
32
- data.processedItems[codenameHelper.escapeCodenameInCodenameIndexer(data.dataToMap.item.system.codename)];
33
- if (processedItem) {
34
- // item was already resolved
35
- return {
36
- item: processedItem,
37
- processedItems: data.processedItems,
38
- preparedItems: data.preparedItems,
39
- processingStartedForCodenames: data.processingStartedForCodenames
40
- };
41
- }
42
-
43
- const preparedItem =
44
- data.preparedItems[codenameHelper.escapeCodenameInCodenameIndexer(data.dataToMap.item.system.codename)];
45
-
46
- const itemInstance = preparedItem?.item;
47
-
48
- if (!itemInstance) {
49
- // item is not present in response
50
- return undefined;
51
- }
52
-
53
- // mapp elements
54
- const elementCodenames = Object.getOwnPropertyNames(data.dataToMap.rawItem.elements);
55
-
56
- for (const elementCodename of elementCodenames) {
57
- const elementWrapper: ElementModels.IElementWrapper = {
58
- system: data.dataToMap.item.system,
59
- rawElement: data.dataToMap.rawItem.elements[elementCodename],
60
- element: elementCodename
61
- };
62
-
63
- const mappedElement = this.mapElement({
64
- elementWrapper: elementWrapper,
65
- item: itemInstance,
66
- preparedItems: data.preparedItems,
67
- processingStartedForCodenames: data.processingStartedForCodenames,
68
- processedItems: data.processedItems
69
- });
70
-
71
- // set mapped elements
72
- itemInstance.elements[elementCodename] = mappedElement;
73
- }
74
-
75
- return {
76
- item: itemInstance as TContentItem,
77
- processedItems: data.processedItems,
78
- preparedItems: data.preparedItems,
79
- processingStartedForCodenames: data.processingStartedForCodenames
80
- };
81
- }
82
-
83
- private mapElement(data: {
84
- elementWrapper: ElementModels.IElementWrapper;
85
- item: IContentItem;
86
- processedItems: IContentItemsContainer<TContentItemType>;
87
- processingStartedForCodenames: string[];
88
- preparedItems: IContentItemWithRawDataContainer;
89
- }): ElementModels.IElement<any> {
90
- const elementType = enumHelper.getEnumFromValue<ElementType>(ElementType, data.elementWrapper.rawElement.type);
91
- if (elementType) {
92
- if (elementType === ElementType.ModularContent) {
93
- return this.mapLinkedItemsElement({
94
- elementWrapper: data.elementWrapper,
95
- preparedItems: data.preparedItems,
96
- processingStartedForCodenames: data.processingStartedForCodenames,
97
- processedItems: data.processedItems
98
- });
99
- }
100
-
101
- if (elementType === ElementType.Text) {
102
- return this.mapTextElement(data.elementWrapper);
103
- }
104
- if (elementType === ElementType.Asset) {
105
- return this.mapAssetsElement(data.elementWrapper);
106
- }
107
-
108
- if (elementType === ElementType.Number) {
109
- return this.mapNumberElement(data.elementWrapper);
110
- }
111
- if (elementType === ElementType.MultipleChoice) {
112
- return this.mapMultipleChoiceElement(data.elementWrapper);
113
- }
114
-
115
- if (elementType === ElementType.DateTime) {
116
- return this.mapDateTimeElement(data.elementWrapper);
117
- }
118
-
119
- if (elementType === ElementType.RichText) {
120
- // add to parent items
121
- return this.mapRichTextElement(
122
- data.elementWrapper,
123
- data.processedItems,
124
- data.processingStartedForCodenames,
125
- data.preparedItems
126
- );
127
- }
128
-
129
- if (elementType === ElementType.UrlSlug) {
130
- return this.mapUrlSlugElement(data.elementWrapper);
131
- }
132
-
133
- if (elementType === ElementType.Taxonomy) {
134
- return this.mapTaxonomyElement(data.elementWrapper);
135
- }
136
-
137
- if (elementType === ElementType.Custom) {
138
- return this.mapCustomElement(data.elementWrapper);
139
- }
140
- }
141
- console.warn(
142
- `Could not map element '${data.elementWrapper.rawElement.name}' of type '${data.elementWrapper.rawElement.type}'. Returning unknown element instead.`
143
- );
144
- return this.mapUnknowElement(data.elementWrapper);
145
- }
146
-
147
- private mapRichTextElement(
148
- elementWrapper: ElementModels.IElementWrapper,
149
- processedItems: IContentItemsContainer<TContentItemType>,
150
- processingStartedForCodenames: string[],
151
- preparedItems: IContentItemWithRawDataContainer
152
- ): Elements.RichTextElement {
153
- const rawElement = elementWrapper.rawElement as Contracts.IRichTextElementContract;
154
-
155
- // get all linked items and linked items codenames nested in rich text
156
- const richTextLinkedItems: IContentItem[] = [];
157
- const richTextLinkedItemsCodenames: string[] = [];
158
-
159
- // The Kontent Delivery API is not guaranteed to return rich-text modular_content array items in the same order in which they appear inside the `value` property.
160
- // We extract the modular_content codenames in the rich-text value and sort the raw modular_content based on that order instead.
161
- const rawModularContentCodenamesMatches = (rawElement.value as string).matchAll(
162
- /<object[^>]+data-codename="(?<codename>[a-z0-9_]*)".*?>/g
163
- );
164
- const rawModularContentCodenamesSorted = Array.from(rawModularContentCodenamesMatches).reduce<string[]>(
165
- (acc, match) => {
166
- if (match.groups && match.groups.codename) {
167
- acc.push(match.groups.codename);
168
- }
169
- return acc;
170
- },
171
- [] as string[]
172
- );
173
- const rawModularContentCodenames = [...rawElement.modular_content].sort(function (a, b) {
174
- return rawModularContentCodenamesSorted.indexOf(a) - rawModularContentCodenamesSorted.indexOf(b);
175
- });
176
-
177
- for (const codename of rawModularContentCodenames) {
178
- richTextLinkedItemsCodenames.push(codename);
179
- // get linked item and check if it exists (it might not be included in response due to 'Depth' parameter)
180
- const preparedData = preparedItems[codename];
181
-
182
- // first try to get existing item
183
- if (this.canMapLinkedItems()) {
184
- const existingLinkedItem = this.getOrSaveLinkedItemForElement(
185
- codename,
186
- rawElement,
187
- processedItems,
188
- processingStartedForCodenames,
189
- preparedItems
190
- );
191
-
192
- if (existingLinkedItem) {
193
- // item was found, add it to linked items
194
- richTextLinkedItems.push(existingLinkedItem);
195
- } else {
196
- // item was not found or not yet resolved
197
- if (preparedData) {
198
- const mappedLinkedItemResult = this.mapElements({
199
- dataToMap: preparedData,
200
- preparedItems: preparedItems,
201
- processingStartedForCodenames: processingStartedForCodenames,
202
- processedItems: processedItems
203
- });
204
-
205
- // add mapped linked item to result
206
- if (mappedLinkedItemResult) {
207
- richTextLinkedItems.push(mappedLinkedItemResult.item);
208
- }
209
- }
210
- }
211
- }
212
- }
213
-
214
- // get rich text images
215
- const richTextImagesResult = this.getRichTextImages(rawElement.images);
216
-
217
- // extract and map links & images
218
- const links: ILink[] = this.mapRichTextLinks(rawElement.links);
219
- const images: IRichTextImage[] = richTextImagesResult.richTextImages;
220
-
221
- // replace asset urls in html
222
- const richTextHtml: string = this.getRichTextHtml(rawElement.value, richTextImagesResult.imageUrlRecords);
223
-
224
- return {
225
- images: images,
226
- linkedItemCodenames: richTextLinkedItemsCodenames,
227
- linkedItems: richTextLinkedItems,
228
- links: links,
229
- name: rawElement.name,
230
- type: ElementType.RichText,
231
- value: richTextHtml
232
- };
233
- }
234
-
235
- private mapDateTimeElement(elementWrapper: ElementModels.IElementWrapper): Elements.DateTimeElement {
236
- const rawElement = elementWrapper.rawElement as Contracts.IDateTimeElementContract;
237
- return {
238
- ...this.buildElement(elementWrapper, ElementType.DateTime, () => rawElement.value),
239
- displayTimeZone: rawElement.display_timezone ?? null
240
- };
241
- }
242
-
243
- private mapMultipleChoiceElement(elementWrapper: ElementModels.IElementWrapper): Elements.MultipleChoiceElement {
244
- return this.buildElement(elementWrapper, ElementType.MultipleChoice, () => elementWrapper.rawElement.value);
245
- }
246
-
247
- private mapNumberElement(elementWrapper: ElementModels.IElementWrapper): Elements.NumberElement {
248
- return this.buildElement(elementWrapper, ElementType.Number, () => {
249
- if (elementWrapper.rawElement.value === 0) {
250
- return 0;
251
- } else if (elementWrapper.rawElement.value) {
252
- return +elementWrapper.rawElement.value;
253
- }
254
- return null;
255
- });
256
- }
257
-
258
- private mapTextElement(elementWrapper: ElementModels.IElementWrapper): Elements.TextElement {
259
- return this.buildElement(elementWrapper, ElementType.Text, () => elementWrapper.rawElement.value);
260
- }
261
-
262
- private mapAssetsElement(elementWrapper: ElementModels.IElementWrapper): Elements.AssetsElement {
263
- return this.buildElement(elementWrapper, ElementType.Asset, () => {
264
- const assetContracts = elementWrapper.rawElement.value as Contracts.IAssetContract[];
265
-
266
- const assets: ElementModels.AssetModel[] = [];
267
-
268
- for (const assetContract of assetContracts) {
269
- let renditions: { [renditionPresetCodename: string]: ElementModels.Rendition } | null = null;
270
-
271
- // get asset url (custom domain may be configured)
272
- const assetUrl: string = this.config.assetsDomain
273
- ? deliveryUrlHelper.replaceAssetDomain(assetContract.url, this.config.assetsDomain)
274
- : assetContract.url;
275
-
276
- if (assetContract.renditions) {
277
- renditions = {};
278
-
279
- for (const renditionPresetKey of Object.keys(assetContract.renditions)) {
280
- const rendition = assetContract.renditions[renditionPresetKey];
281
-
282
- renditions[renditionPresetKey] = {
283
- ...rendition,
284
- url: `${assetUrl}?${rendition.query}` // enhance rendition with absolute url
285
- };
286
- }
287
- }
288
-
289
- const renditionToBeApplied: ElementModels.Rendition | null =
290
- (this.config.defaultRenditionPreset && renditions?.[this.config.defaultRenditionPreset]) || null;
291
-
292
- const finalUrl = renditionToBeApplied?.url ?? assetUrl;
293
-
294
- const asset: ElementModels.AssetModel = {
295
- ...assetContract,
296
- url: finalUrl, // use custom url of asset which may contain custom domain and applied rendition
297
- renditions
298
- };
299
-
300
- assets.push(asset);
301
- }
302
-
303
- return assets;
304
- });
305
- }
306
-
307
- private mapTaxonomyElement(elementWrapper: ElementModels.IElementWrapper): Elements.TaxonomyElement {
308
- return {
309
- ...this.buildElement(elementWrapper, ElementType.Taxonomy, () => elementWrapper.rawElement.value),
310
- taxonomyGroup: elementWrapper.rawElement.taxonomy_group ?? ''
311
- };
312
- }
313
-
314
- private mapUnknowElement(elementWrapper: ElementModels.IElementWrapper): Elements.UnknownElement {
315
- return this.buildElement(elementWrapper, ElementType.Unknown, () => elementWrapper.rawElement.value);
316
- }
317
-
318
- private mapCustomElement(
319
- elementWrapper: ElementModels.IElementWrapper
320
- ): Elements.CustomElement | ElementModels.IElement<string> {
321
- // try to find element resolver
322
- if (this.config.elementResolver) {
323
- const elementResolverValue = this.config.elementResolver(elementWrapper);
324
-
325
- if (elementResolverValue) {
326
- return this.buildElement(elementWrapper, ElementType.Custom, () => elementResolverValue);
327
- }
328
- }
329
-
330
- return this.buildElement(elementWrapper, ElementType.Custom, () => elementWrapper.rawElement.value);
331
- }
332
-
333
- private mapUrlSlugElement(elementWrapper: ElementModels.IElementWrapper): Elements.UrlSlugElement {
334
- return this.buildElement(elementWrapper, ElementType.UrlSlug, () => elementWrapper.rawElement.value);
335
- }
336
-
337
- private mapLinkedItemsElement(data: {
338
- elementWrapper: ElementModels.IElementWrapper;
339
- processedItems: IContentItemsContainer<TContentItemType>;
340
- processingStartedForCodenames: string[];
341
- preparedItems: IContentItemWithRawDataContainer;
342
- }): Elements.LinkedItemsElement<any> {
343
- // prepare linked items
344
- const linkedItems: IContentItem[] = [];
345
-
346
- // value = array of item codenames
347
- const linkedItemCodenames = data.elementWrapper.rawElement.value as string[];
348
-
349
- for (const codename of linkedItemCodenames) {
350
- if (this.canMapLinkedItems()) {
351
- const linkedItem = this.getOrSaveLinkedItemForElement(
352
- codename,
353
- data.elementWrapper.rawElement,
354
- data.processedItems,
355
- data.processingStartedForCodenames,
356
- data.preparedItems
357
- );
358
- if (linkedItem) {
359
- // add item to result
360
- linkedItems.push(linkedItem);
361
- }
362
- }
363
- }
364
-
365
- return {
366
- ...this.buildElement(data.elementWrapper, ElementType.ModularContent, () => linkedItemCodenames),
367
- linkedItems: linkedItems
368
- };
369
- }
370
-
371
- private getOrSaveLinkedItemForElement(
372
- codename: string,
373
- element: Contracts.IElementContract,
374
- processedItems: IContentItemsContainer<TContentItemType>,
375
- mappingStartedForCodenames: string[],
376
- preparedItems: IContentItemWithRawDataContainer
377
- ): IContentItem | undefined {
378
- const escapedCodename = codenameHelper.escapeCodenameInCodenameIndexer(codename);
379
-
380
- // first check if item was already resolved and return it if it was
381
- const processedItem = processedItems[escapedCodename];
382
-
383
- if (processedItem) {
384
- // item was already resolved
385
- return processedItem;
386
- }
387
-
388
- const preparedItem = preparedItems[escapedCodename];
389
-
390
- if (mappingStartedForCodenames.includes(codename)) {
391
- return preparedItem?.item;
392
- }
393
-
394
- mappingStartedForCodenames.push(codename);
395
-
396
- // throw error if item is not in response and errors are not skipped
397
- if (!preparedItem) {
398
- return undefined;
399
- }
400
-
401
- let mappedLinkedItem: TContentItemType | undefined;
402
-
403
- // original resolving if item is still undefined
404
- const mappedLinkedItemResult = this.mapElements({
405
- dataToMap: preparedItem,
406
- preparedItems: preparedItems,
407
- processingStartedForCodenames: mappingStartedForCodenames,
408
- processedItems: processedItems
409
- });
410
-
411
- if (mappedLinkedItemResult) {
412
- mappedLinkedItem = mappedLinkedItemResult.item;
413
-
414
- // add to processed items
415
- processedItems[escapedCodename] = mappedLinkedItem;
416
- }
417
-
418
- return mappedLinkedItem;
419
- }
420
-
421
- private mapRichTextLinks(linksJson: Contracts.IRichTextElementLinkWrapperContract): ILink[] {
422
- const links: ILink[] = [];
423
-
424
- for (const linkId of Object.keys(linksJson)) {
425
- const linkRaw = linksJson[linkId];
426
- links.push({
427
- codename: linkRaw.codename,
428
- linkId: linkId,
429
- urlSlug: linkRaw.url_slug,
430
- type: linkRaw.type
431
- });
432
- }
433
-
434
- return links;
435
- }
436
-
437
- private getRichTextHtml(richTextHtml: string, richTextImageRecords: IRichTextImageUrlRecord[]): string {
438
- for (const richTextImageRecord of richTextImageRecords) {
439
- // replace rich text image url if it differs
440
- if (richTextImageRecord.newUrl !== richTextImageRecord.originalUrl) {
441
- richTextHtml = richTextHtml.replace(
442
- new RegExp(richTextImageRecord.originalUrl, 'g'),
443
- richTextImageRecord.newUrl
444
- );
445
- }
446
- }
447
-
448
- return richTextHtml;
449
- }
450
-
451
- private getRichTextImages(imagesJson: Contracts.IRichTextElementImageWrapperContract): {
452
- richTextImages: IRichTextImage[];
453
- imageUrlRecords: IRichTextImageUrlRecord[];
454
- } {
455
- const images: IRichTextImage[] = [];
456
- const imageUrlRecords: IRichTextImageUrlRecord[] = [];
457
-
458
- for (const imageId of Object.keys(imagesJson)) {
459
- const imageRaw = imagesJson[imageId];
460
-
461
- // image may contain custom asset domain
462
- const imageUrl: string = this.config.assetsDomain
463
- ? deliveryUrlHelper.replaceAssetDomain(imageRaw.url, this.config.assetsDomain)
464
- : imageRaw.url;
465
-
466
- images.push({
467
- description: imageRaw.description ?? null,
468
- imageId: imageRaw.image_id,
469
- url: imageUrl,
470
- height: imageRaw.height ?? null,
471
- width: imageRaw.width ?? null
472
- });
473
-
474
- imageUrlRecords.push({
475
- originalUrl: imageRaw.url,
476
- newUrl: imageUrl
477
- });
478
- }
479
-
480
- return {
481
- imageUrlRecords: imageUrlRecords,
482
- richTextImages: images
483
- };
484
- }
485
-
486
- private buildElement<TValue>(
487
- elementWrapper: ElementModels.IElementWrapper,
488
- type: ElementType,
489
- valueFactory: () => TValue
490
- ): ElementModels.IElement<TValue> {
491
- return {
492
- name: elementWrapper.rawElement.name,
493
- type: type,
494
- value: valueFactory()
495
- };
496
- }
497
-
498
- private canMapLinkedItems(): boolean {
499
- if (!this.config.linkedItemsReferenceHandler) {
500
- return true;
501
- }
502
- return this.config.linkedItemsReferenceHandler === 'map';
503
- }
504
- }
1
+ import { codenameHelper, deliveryUrlHelper, enumHelper } from '../utilities';
2
+
3
+ import { IDeliveryClientConfig } from '../config';
4
+ import { Contracts } from '../contracts';
5
+ import { ElementModels, Elements, ElementType } from '../elements';
6
+ import {
7
+ IContentItem,
8
+ IContentItemsContainer,
9
+ IContentItemWithRawDataContainer,
10
+ IContentItemWithRawElements,
11
+ ILink,
12
+ IMapElementsResult,
13
+ IRichTextImage
14
+ } from '../models';
15
+
16
+ interface IRichTextImageUrlRecord {
17
+ originalUrl: string;
18
+ newUrl: string;
19
+ }
20
+
21
+ export class ElementMapper<TContentItemType extends IContentItem> {
22
+ constructor(private readonly config: IDeliveryClientConfig) {}
23
+
24
+ mapElements<TContentItem extends TContentItemType = TContentItemType>(data: {
25
+ dataToMap: IContentItemWithRawElements;
26
+ processedItems: IContentItemsContainer<TContentItem>;
27
+ processingStartedForCodenames: string[];
28
+ preparedItems: IContentItemWithRawDataContainer;
29
+ }): IMapElementsResult<TContentItem, TContentItemType> | undefined {
30
+ // return processed item to avoid infinite recursion
31
+ const processedItem =
32
+ data.processedItems[codenameHelper.escapeCodenameInCodenameIndexer(data.dataToMap.item.system.codename)];
33
+ if (processedItem) {
34
+ // item was already resolved
35
+ return {
36
+ item: processedItem,
37
+ processedItems: data.processedItems,
38
+ preparedItems: data.preparedItems,
39
+ processingStartedForCodenames: data.processingStartedForCodenames
40
+ };
41
+ }
42
+
43
+ const preparedItem =
44
+ data.preparedItems[codenameHelper.escapeCodenameInCodenameIndexer(data.dataToMap.item.system.codename)];
45
+
46
+ const itemInstance = preparedItem?.item;
47
+
48
+ if (!itemInstance) {
49
+ // item is not present in response
50
+ return undefined;
51
+ }
52
+
53
+ // mapp elements
54
+ const elementCodenames = Object.getOwnPropertyNames(data.dataToMap.rawItem.elements);
55
+
56
+ for (const elementCodename of elementCodenames) {
57
+ const elementWrapper: ElementModels.IElementWrapper = {
58
+ system: data.dataToMap.item.system,
59
+ rawElement: data.dataToMap.rawItem.elements[elementCodename],
60
+ element: elementCodename
61
+ };
62
+
63
+ const mappedElement = this.mapElement({
64
+ elementWrapper: elementWrapper,
65
+ item: itemInstance,
66
+ preparedItems: data.preparedItems,
67
+ processingStartedForCodenames: data.processingStartedForCodenames,
68
+ processedItems: data.processedItems
69
+ });
70
+
71
+ // set mapped elements
72
+ itemInstance.elements[elementCodename] = mappedElement;
73
+ }
74
+
75
+ return {
76
+ item: itemInstance as TContentItem,
77
+ processedItems: data.processedItems,
78
+ preparedItems: data.preparedItems,
79
+ processingStartedForCodenames: data.processingStartedForCodenames
80
+ };
81
+ }
82
+
83
+ private mapElement(data: {
84
+ elementWrapper: ElementModels.IElementWrapper;
85
+ item: IContentItem;
86
+ processedItems: IContentItemsContainer<TContentItemType>;
87
+ processingStartedForCodenames: string[];
88
+ preparedItems: IContentItemWithRawDataContainer;
89
+ }): ElementModels.IElement<any> {
90
+ const elementType = enumHelper.getEnumFromValue<ElementType>(ElementType, data.elementWrapper.rawElement.type);
91
+ if (elementType) {
92
+ if (elementType === ElementType.ModularContent) {
93
+ return this.mapLinkedItemsElement({
94
+ elementWrapper: data.elementWrapper,
95
+ preparedItems: data.preparedItems,
96
+ processingStartedForCodenames: data.processingStartedForCodenames,
97
+ processedItems: data.processedItems
98
+ });
99
+ }
100
+
101
+ if (elementType === ElementType.Text) {
102
+ return this.mapTextElement(data.elementWrapper);
103
+ }
104
+ if (elementType === ElementType.Asset) {
105
+ return this.mapAssetsElement(data.elementWrapper);
106
+ }
107
+
108
+ if (elementType === ElementType.Number) {
109
+ return this.mapNumberElement(data.elementWrapper);
110
+ }
111
+ if (elementType === ElementType.MultipleChoice) {
112
+ return this.mapMultipleChoiceElement(data.elementWrapper);
113
+ }
114
+
115
+ if (elementType === ElementType.DateTime) {
116
+ return this.mapDateTimeElement(data.elementWrapper);
117
+ }
118
+
119
+ if (elementType === ElementType.RichText) {
120
+ // add to parent items
121
+ return this.mapRichTextElement(
122
+ data.elementWrapper,
123
+ data.processedItems,
124
+ data.processingStartedForCodenames,
125
+ data.preparedItems
126
+ );
127
+ }
128
+
129
+ if (elementType === ElementType.UrlSlug) {
130
+ return this.mapUrlSlugElement(data.elementWrapper);
131
+ }
132
+
133
+ if (elementType === ElementType.Taxonomy) {
134
+ return this.mapTaxonomyElement(data.elementWrapper);
135
+ }
136
+
137
+ if (elementType === ElementType.Custom) {
138
+ return this.mapCustomElement(data.elementWrapper);
139
+ }
140
+ }
141
+ console.warn(
142
+ `Could not map element '${data.elementWrapper.rawElement.name}' of type '${data.elementWrapper.rawElement.type}'. Returning unknown element instead.`
143
+ );
144
+ return this.mapUnknowElement(data.elementWrapper);
145
+ }
146
+
147
+ private mapRichTextElement(
148
+ elementWrapper: ElementModels.IElementWrapper,
149
+ processedItems: IContentItemsContainer<TContentItemType>,
150
+ processingStartedForCodenames: string[],
151
+ preparedItems: IContentItemWithRawDataContainer
152
+ ): Elements.RichTextElement {
153
+ const rawElement = elementWrapper.rawElement as Contracts.IRichTextElementContract;
154
+
155
+ // get all linked items and linked items codenames nested in rich text
156
+ const richTextLinkedItems: IContentItem[] = [];
157
+ const richTextLinkedItemsCodenames: string[] = [];
158
+
159
+ // The Kontent Delivery API is not guaranteed to return rich-text modular_content array items in the same order in which they appear inside the `value` property.
160
+ // We extract the modular_content codenames in the rich-text value and sort the raw modular_content based on that order instead.
161
+ const rawModularContentCodenamesMatches = (rawElement.value as string).matchAll(
162
+ /<object[^>]+data-codename="(?<codename>[a-z0-9_]*)".*?>/g
163
+ );
164
+ const rawModularContentCodenamesSorted = Array.from(rawModularContentCodenamesMatches).reduce<string[]>(
165
+ (acc, match) => {
166
+ if (match.groups && match.groups.codename) {
167
+ acc.push(match.groups.codename);
168
+ }
169
+ return acc;
170
+ },
171
+ [] as string[]
172
+ );
173
+ const rawModularContentCodenames = [...rawElement.modular_content].sort(function (a, b) {
174
+ return rawModularContentCodenamesSorted.indexOf(a) - rawModularContentCodenamesSorted.indexOf(b);
175
+ });
176
+
177
+ for (const codename of rawModularContentCodenames) {
178
+ richTextLinkedItemsCodenames.push(codename);
179
+ // get linked item and check if it exists (it might not be included in response due to 'Depth' parameter)
180
+ const preparedData = preparedItems[codename];
181
+
182
+ // first try to get existing item
183
+ if (this.canMapLinkedItems()) {
184
+ const existingLinkedItem = this.getOrSaveLinkedItemForElement(
185
+ codename,
186
+ rawElement,
187
+ processedItems,
188
+ processingStartedForCodenames,
189
+ preparedItems
190
+ );
191
+
192
+ if (existingLinkedItem) {
193
+ // item was found, add it to linked items
194
+ richTextLinkedItems.push(existingLinkedItem);
195
+ } else {
196
+ // item was not found or not yet resolved
197
+ if (preparedData) {
198
+ const mappedLinkedItemResult = this.mapElements({
199
+ dataToMap: preparedData,
200
+ preparedItems: preparedItems,
201
+ processingStartedForCodenames: processingStartedForCodenames,
202
+ processedItems: processedItems
203
+ });
204
+
205
+ // add mapped linked item to result
206
+ if (mappedLinkedItemResult) {
207
+ richTextLinkedItems.push(mappedLinkedItemResult.item);
208
+ }
209
+ }
210
+ }
211
+ }
212
+ }
213
+
214
+ // get rich text images
215
+ const richTextImagesResult = this.getRichTextImages(rawElement.images);
216
+
217
+ // extract and map links & images
218
+ const links: ILink[] = this.mapRichTextLinks(rawElement.links);
219
+ const images: IRichTextImage[] = richTextImagesResult.richTextImages;
220
+
221
+ // replace asset urls in html
222
+ const richTextHtml: string = this.getRichTextHtml(rawElement.value, richTextImagesResult.imageUrlRecords);
223
+
224
+ return {
225
+ images: images,
226
+ linkedItemCodenames: richTextLinkedItemsCodenames,
227
+ linkedItems: richTextLinkedItems,
228
+ links: links,
229
+ name: rawElement.name,
230
+ type: ElementType.RichText,
231
+ value: richTextHtml
232
+ };
233
+ }
234
+
235
+ private mapDateTimeElement(elementWrapper: ElementModels.IElementWrapper): Elements.DateTimeElement {
236
+ const rawElement = elementWrapper.rawElement as Contracts.IDateTimeElementContract;
237
+ return {
238
+ ...this.buildElement(elementWrapper, ElementType.DateTime, () => rawElement.value),
239
+ displayTimeZone: rawElement.display_timezone ?? null
240
+ };
241
+ }
242
+
243
+ private mapMultipleChoiceElement(elementWrapper: ElementModels.IElementWrapper): Elements.MultipleChoiceElement {
244
+ return this.buildElement(elementWrapper, ElementType.MultipleChoice, () => elementWrapper.rawElement.value);
245
+ }
246
+
247
+ private mapNumberElement(elementWrapper: ElementModels.IElementWrapper): Elements.NumberElement {
248
+ return this.buildElement(elementWrapper, ElementType.Number, () => {
249
+ if (elementWrapper.rawElement.value === 0) {
250
+ return 0;
251
+ } else if (elementWrapper.rawElement.value) {
252
+ return +elementWrapper.rawElement.value;
253
+ }
254
+ return null;
255
+ });
256
+ }
257
+
258
+ private mapTextElement(elementWrapper: ElementModels.IElementWrapper): Elements.TextElement {
259
+ return this.buildElement(elementWrapper, ElementType.Text, () => elementWrapper.rawElement.value);
260
+ }
261
+
262
+ private mapAssetsElement(elementWrapper: ElementModels.IElementWrapper): Elements.AssetsElement {
263
+ return this.buildElement(elementWrapper, ElementType.Asset, () => {
264
+ const assetContracts = elementWrapper.rawElement.value as Contracts.IAssetContract[];
265
+
266
+ const assets: ElementModels.AssetModel[] = [];
267
+
268
+ for (const assetContract of assetContracts) {
269
+ let renditions: { [renditionPresetCodename: string]: ElementModels.Rendition } | null = null;
270
+
271
+ // get asset url (custom domain may be configured)
272
+ const assetUrl: string = this.config.assetsDomain
273
+ ? deliveryUrlHelper.replaceAssetDomain(assetContract.url, this.config.assetsDomain)
274
+ : assetContract.url;
275
+
276
+ if (assetContract.renditions) {
277
+ renditions = {};
278
+
279
+ for (const renditionPresetKey of Object.keys(assetContract.renditions)) {
280
+ const rendition = assetContract.renditions[renditionPresetKey];
281
+
282
+ renditions[renditionPresetKey] = {
283
+ ...rendition,
284
+ url: `${assetUrl}?${rendition.query}` // enhance rendition with absolute url
285
+ };
286
+ }
287
+ }
288
+
289
+ const renditionToBeApplied: ElementModels.Rendition | null =
290
+ (this.config.defaultRenditionPreset && renditions?.[this.config.defaultRenditionPreset]) || null;
291
+
292
+ const finalUrl = renditionToBeApplied?.url ?? assetUrl;
293
+
294
+ const asset: ElementModels.AssetModel = {
295
+ ...assetContract,
296
+ url: finalUrl, // use custom url of asset which may contain custom domain and applied rendition
297
+ renditions
298
+ };
299
+
300
+ assets.push(asset);
301
+ }
302
+
303
+ return assets;
304
+ });
305
+ }
306
+
307
+ private mapTaxonomyElement(elementWrapper: ElementModels.IElementWrapper): Elements.TaxonomyElement {
308
+ return {
309
+ ...this.buildElement(elementWrapper, ElementType.Taxonomy, () => elementWrapper.rawElement.value),
310
+ taxonomyGroup: elementWrapper.rawElement.taxonomy_group ?? ''
311
+ };
312
+ }
313
+
314
+ private mapUnknowElement(elementWrapper: ElementModels.IElementWrapper): Elements.UnknownElement {
315
+ return this.buildElement(elementWrapper, ElementType.Unknown, () => elementWrapper.rawElement.value);
316
+ }
317
+
318
+ private mapCustomElement(
319
+ elementWrapper: ElementModels.IElementWrapper
320
+ ): Elements.CustomElement | ElementModels.IElement<string> {
321
+ // try to find element resolver
322
+ if (this.config.elementResolver) {
323
+ const elementResolverValue = this.config.elementResolver(elementWrapper);
324
+
325
+ if (elementResolverValue) {
326
+ return this.buildElement(elementWrapper, ElementType.Custom, () => elementResolverValue);
327
+ }
328
+ }
329
+
330
+ return this.buildElement(elementWrapper, ElementType.Custom, () => elementWrapper.rawElement.value);
331
+ }
332
+
333
+ private mapUrlSlugElement(elementWrapper: ElementModels.IElementWrapper): Elements.UrlSlugElement {
334
+ return this.buildElement(elementWrapper, ElementType.UrlSlug, () => elementWrapper.rawElement.value);
335
+ }
336
+
337
+ private mapLinkedItemsElement(data: {
338
+ elementWrapper: ElementModels.IElementWrapper;
339
+ processedItems: IContentItemsContainer<TContentItemType>;
340
+ processingStartedForCodenames: string[];
341
+ preparedItems: IContentItemWithRawDataContainer;
342
+ }): Elements.LinkedItemsElement<any> {
343
+ // prepare linked items
344
+ const linkedItems: IContentItem[] = [];
345
+
346
+ // value = array of item codenames
347
+ const linkedItemCodenames = data.elementWrapper.rawElement.value as string[];
348
+
349
+ for (const codename of linkedItemCodenames) {
350
+ if (this.canMapLinkedItems()) {
351
+ const linkedItem = this.getOrSaveLinkedItemForElement(
352
+ codename,
353
+ data.elementWrapper.rawElement,
354
+ data.processedItems,
355
+ data.processingStartedForCodenames,
356
+ data.preparedItems
357
+ );
358
+ if (linkedItem) {
359
+ // add item to result
360
+ linkedItems.push(linkedItem);
361
+ }
362
+ }
363
+ }
364
+
365
+ return {
366
+ ...this.buildElement(data.elementWrapper, ElementType.ModularContent, () => linkedItemCodenames),
367
+ linkedItems: linkedItems
368
+ };
369
+ }
370
+
371
+ private getOrSaveLinkedItemForElement(
372
+ codename: string,
373
+ element: Contracts.IElementContract,
374
+ processedItems: IContentItemsContainer<TContentItemType>,
375
+ mappingStartedForCodenames: string[],
376
+ preparedItems: IContentItemWithRawDataContainer
377
+ ): IContentItem | undefined {
378
+ const escapedCodename = codenameHelper.escapeCodenameInCodenameIndexer(codename);
379
+
380
+ // first check if item was already resolved and return it if it was
381
+ const processedItem = processedItems[escapedCodename];
382
+
383
+ if (processedItem) {
384
+ // item was already resolved
385
+ return processedItem;
386
+ }
387
+
388
+ const preparedItem = preparedItems[escapedCodename];
389
+
390
+ if (mappingStartedForCodenames.includes(codename)) {
391
+ return preparedItem?.item;
392
+ }
393
+
394
+ mappingStartedForCodenames.push(codename);
395
+
396
+ // throw error if item is not in response and errors are not skipped
397
+ if (!preparedItem) {
398
+ return undefined;
399
+ }
400
+
401
+ let mappedLinkedItem: TContentItemType | undefined;
402
+
403
+ // original resolving if item is still undefined
404
+ const mappedLinkedItemResult = this.mapElements({
405
+ dataToMap: preparedItem,
406
+ preparedItems: preparedItems,
407
+ processingStartedForCodenames: mappingStartedForCodenames,
408
+ processedItems: processedItems
409
+ });
410
+
411
+ if (mappedLinkedItemResult) {
412
+ mappedLinkedItem = mappedLinkedItemResult.item;
413
+
414
+ // add to processed items
415
+ processedItems[escapedCodename] = mappedLinkedItem;
416
+ }
417
+
418
+ return mappedLinkedItem;
419
+ }
420
+
421
+ private mapRichTextLinks(linksJson: Contracts.IRichTextElementLinkWrapperContract): ILink[] {
422
+ const links: ILink[] = [];
423
+
424
+ for (const linkId of Object.keys(linksJson)) {
425
+ const linkRaw = linksJson[linkId];
426
+ links.push({
427
+ codename: linkRaw.codename,
428
+ linkId: linkId,
429
+ urlSlug: linkRaw.url_slug,
430
+ type: linkRaw.type
431
+ });
432
+ }
433
+
434
+ return links;
435
+ }
436
+
437
+ private getRichTextHtml(richTextHtml: string, richTextImageRecords: IRichTextImageUrlRecord[]): string {
438
+ for (const richTextImageRecord of richTextImageRecords) {
439
+ // replace rich text image url if it differs
440
+ if (richTextImageRecord.newUrl !== richTextImageRecord.originalUrl) {
441
+ richTextHtml = richTextHtml.replace(
442
+ new RegExp(richTextImageRecord.originalUrl, 'g'),
443
+ richTextImageRecord.newUrl
444
+ );
445
+ }
446
+ }
447
+
448
+ return richTextHtml;
449
+ }
450
+
451
+ private getRichTextImages(imagesJson: Contracts.IRichTextElementImageWrapperContract): {
452
+ richTextImages: IRichTextImage[];
453
+ imageUrlRecords: IRichTextImageUrlRecord[];
454
+ } {
455
+ const images: IRichTextImage[] = [];
456
+ const imageUrlRecords: IRichTextImageUrlRecord[] = [];
457
+
458
+ for (const imageId of Object.keys(imagesJson)) {
459
+ const imageRaw = imagesJson[imageId];
460
+
461
+ // image may contain custom asset domain
462
+ const imageUrl: string = this.config.assetsDomain
463
+ ? deliveryUrlHelper.replaceAssetDomain(imageRaw.url, this.config.assetsDomain)
464
+ : imageRaw.url;
465
+
466
+ images.push({
467
+ description: imageRaw.description ?? null,
468
+ imageId: imageRaw.image_id,
469
+ url: imageUrl,
470
+ height: imageRaw.height ?? null,
471
+ width: imageRaw.width ?? null
472
+ });
473
+
474
+ imageUrlRecords.push({
475
+ originalUrl: imageRaw.url,
476
+ newUrl: imageUrl
477
+ });
478
+ }
479
+
480
+ return {
481
+ imageUrlRecords: imageUrlRecords,
482
+ richTextImages: images
483
+ };
484
+ }
485
+
486
+ private buildElement<TValue>(
487
+ elementWrapper: ElementModels.IElementWrapper,
488
+ type: ElementType,
489
+ valueFactory: () => TValue
490
+ ): ElementModels.IElement<TValue> {
491
+ return {
492
+ name: elementWrapper.rawElement.name,
493
+ type: type,
494
+ value: valueFactory()
495
+ };
496
+ }
497
+
498
+ private canMapLinkedItems(): boolean {
499
+ if (!this.config.linkedItemsReferenceHandler) {
500
+ return true;
501
+ }
502
+ return this.config.linkedItemsReferenceHandler === 'map';
503
+ }
504
+ }