From c8e70e402a3b81fcdf965c68fd51842e39958bef Mon Sep 17 00:00:00 2001 From: Richard Sustek Date: Sun, 12 Dec 2021 15:43:00 +0100 Subject: [PATCH 1/3] feat: Adds support for custom asset domains (fixes https://github.com/Kentico/kontent-delivery-sdk-js/issues/333) --- lib/config/delivery-configs.ts | 5 ++ lib/mappers/element.mapper.ts | 62 +++++++++++++++-- lib/utilities/delivery-url.helper.ts | 17 +++++ lib/utilities/index.ts | 1 + package-lock.json | 48 ++++++++++++-- package.json | 4 +- .../assets/custom-assets-domain.spec.json | 66 +++++++++++++++++++ .../assets/custom-assets-domain.spec.ts | 46 +++++++++++++ 8 files changed, 238 insertions(+), 11 deletions(-) create mode 100644 lib/utilities/delivery-url.helper.ts create mode 100644 test/browser/isolated-tests/elements/assets/custom-assets-domain.spec.json create mode 100644 test/browser/isolated-tests/elements/assets/custom-assets-domain.spec.ts diff --git a/lib/config/delivery-configs.ts b/lib/config/delivery-configs.ts index a3d9166a..018356a7 100644 --- a/lib/config/delivery-configs.ts +++ b/lib/config/delivery-configs.ts @@ -85,4 +85,9 @@ export interface IDeliveryClientConfig { * with circular refences) */ linkedItemsReferenceHandler?: LinkedItemsReferenceHandler; + + /** + * Sets custom domain for assets + */ + assetsDomain?: string; } diff --git a/lib/mappers/element.mapper.ts b/lib/mappers/element.mapper.ts index fcee9b4e..71fb8a41 100644 --- a/lib/mappers/element.mapper.ts +++ b/lib/mappers/element.mapper.ts @@ -1,4 +1,5 @@ import { enumHelper } from '@kentico/kontent-core'; +import { deliveryUrlHelper } from '../../lib/utilities'; import { IDeliveryClientConfig } from '../config'; import { Contracts } from '../contracts'; @@ -13,6 +14,11 @@ import { IContentItemWithRawElements } from '../models'; +interface IRichTextImageUrlRecord { + originalUrl: string; + newUrl: string; +} + export class ElementMapper { constructor(private readonly config: IDeliveryClientConfig) {} @@ -185,9 +191,15 @@ export class ElementMapper { } } + // get rich text images + const richTextImagesResult = this.getRichTextImages(rawElement.images); + // extract and map links & images const links: ILink[] = this.mapRichTextLinks(rawElement.links); - const images: IRichTextImage[] = this.mapRichTextImages(rawElement.images); + const images: IRichTextImage[] = richTextImagesResult.richTextImages; + + // replace asset urls in html + const richTextHtml: string = this.getRichTextHtml(rawElement.value, richTextImagesResult.imageUrlRecords); return { images: images, @@ -195,7 +207,7 @@ export class ElementMapper { links: links, name: rawElement.name, type: ElementType.RichText, - value: rawElement.value + value: richTextHtml }; } @@ -231,6 +243,11 @@ export class ElementMapper { for (const assetContract of assetContracts) { let renditions: { [renditionPresetCodename: string]: ElementModels.Rendition } | null = null; + // get asset url (custom domain may be configured) + const assetUrl: string = this.config.assetsDomain + ? deliveryUrlHelper.replaceAssetDomain(assetContract.url, this.config.assetsDomain) + : assetContract.url; + if (assetContract.renditions) { renditions = {}; @@ -239,13 +256,14 @@ export class ElementMapper { renditions[renditionKey] = { ...rendition, - url: `${assetContract.url}?${rendition.query}` // enhance rendition with absolute url + url: `${assetUrl}?${rendition.query}` // enhance rendition with absolute url }; } } const asset: ElementModels.AssetModel = { ...assetContract, + url: assetUrl, // use custom url of asset which may contain custom domain renditions }; @@ -384,21 +402,53 @@ export class ElementMapper { return links; } - private mapRichTextImages(imagesJson: Contracts.IRichTextElementImageWrapperContract): IRichTextImage[] { + private getRichTextHtml(richTextHtml: string, richTextImageRecords: IRichTextImageUrlRecord[]): string { + for (const richTextImageRecord of richTextImageRecords) { + // replace rich text image url if it differs + if (richTextImageRecord.newUrl !== richTextImageRecord.originalUrl) { + richTextHtml = richTextHtml.replace( + new RegExp(richTextImageRecord.originalUrl, 'g'), + richTextImageRecord.newUrl + ); + } + } + + return richTextHtml; + } + + private getRichTextImages(imagesJson: Contracts.IRichTextElementImageWrapperContract): { + richTextImages: IRichTextImage[]; + imageUrlRecords: IRichTextImageUrlRecord[]; + } { const images: IRichTextImage[] = []; + const imageUrlRecords: IRichTextImageUrlRecord[] = []; for (const imageId of Object.keys(imagesJson)) { const imageRaw = imagesJson[imageId]; + + // image may contain custom asset domain + const imageUrl: string = this.config.assetsDomain + ? deliveryUrlHelper.replaceAssetDomain(imageRaw.url, this.config.assetsDomain) + : imageRaw.url; + images.push({ description: imageRaw.description ?? null, imageId: imageRaw.image_id, - url: imageRaw.url, + url: imageUrl, height: imageRaw.height ?? null, width: imageRaw.width ?? null }); + + imageUrlRecords.push({ + originalUrl: imageRaw.url, + newUrl: imageUrl + }); } - return images; + return { + imageUrlRecords: imageUrlRecords, + richTextImages: images + }; } private resolveElementMap( diff --git a/lib/utilities/delivery-url.helper.ts b/lib/utilities/delivery-url.helper.ts new file mode 100644 index 00000000..ac2c68e1 --- /dev/null +++ b/lib/utilities/delivery-url.helper.ts @@ -0,0 +1,17 @@ +import * as urlParse from 'url-parse'; + +export class DeliveryUrlHelper { + replaceAssetDomain(originalAssetUrl: string, customDomain: string): string { + const urlPath = this.getUrlPathname(originalAssetUrl); + + return `${customDomain}${urlPath}`; + } + + getUrlPathname(url: string): string { + const parsedUrl = urlParse(url); + + return parsedUrl.pathname; + } +} + +export const deliveryUrlHelper = new DeliveryUrlHelper(); diff --git a/lib/utilities/index.ts b/lib/utilities/index.ts index 01ad68a8..bc96bb65 100644 --- a/lib/utilities/index.ts +++ b/lib/utilities/index.ts @@ -1,2 +1,3 @@ export * from './linked-items.helper'; export * from './guid.helper'; +export * from './delivery-url.helper'; diff --git a/package-lock.json b/package-lock.json index 02418fcd..b2a5605c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,11 +10,13 @@ "license": "MIT", "dependencies": { "@kentico/kontent-core": "9.4.0", + "url-parse": "1.5.3", "uuid": "8.3.2" }, "devDependencies": { "@types/jasmine": "3.10.2", "@types/node": "16.11.12", + "@types/url-parse": "1.4.5", "@types/uuid": "8.3.3", "colors": "1.4.0", "jasmine-core": "3.10.1", @@ -1906,6 +1908,12 @@ "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", "dev": true }, + "node_modules/@types/url-parse": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@types/url-parse/-/url-parse-1.4.5.tgz", + "integrity": "sha512-8Wje3itJpk/FX+QItca9vjNLjGx5jlEYBw/CpMi03Fphk2DSVeZDUqWTE81BeCI5Bl6Z+zmA1O9L/8e3ZUSeLg==", + "dev": true + }, "node_modules/@types/uuid": { "version": "8.3.3", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.3.tgz", @@ -7000,6 +7008,11 @@ "node": ">=0.4.x" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, "node_modules/quick-lru": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", @@ -7313,8 +7326,7 @@ "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", - "dev": true + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" }, "node_modules/resolve": { "version": "1.12.0", @@ -8407,6 +8419,15 @@ "querystring": "0.2.0" } }, + "node_modules/url-parse": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz", + "integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/url/node_modules/punycode": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", @@ -10213,6 +10234,12 @@ "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", "dev": true }, + "@types/url-parse": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@types/url-parse/-/url-parse-1.4.5.tgz", + "integrity": "sha512-8Wje3itJpk/FX+QItca9vjNLjGx5jlEYBw/CpMi03Fphk2DSVeZDUqWTE81BeCI5Bl6Z+zmA1O9L/8e3ZUSeLg==", + "dev": true + }, "@types/uuid": { "version": "8.3.3", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.3.tgz", @@ -14169,6 +14196,11 @@ "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", "dev": true }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, "quick-lru": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", @@ -14425,8 +14457,7 @@ "requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", - "dev": true + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" }, "resolve": { "version": "1.12.0", @@ -15248,6 +15279,15 @@ } } }, + "url-parse": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz", + "integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==", + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "util": { "version": "0.12.4", "resolved": "https://registry.npmjs.org/util/-/util-0.12.4.tgz", diff --git a/package.json b/package.json index e8ef2219..5e37569d 100644 --- a/package.json +++ b/package.json @@ -72,9 +72,11 @@ }, "dependencies": { "@kentico/kontent-core": "9.4.0", - "uuid": "8.3.2" + "uuid": "8.3.2", + "url-parse": "1.5.3" }, "devDependencies": { + "@types/url-parse": "1.4.5", "@types/jasmine": "3.10.2", "@types/node": "16.11.12", "@types/uuid": "8.3.3", diff --git a/test/browser/isolated-tests/elements/assets/custom-assets-domain.spec.json b/test/browser/isolated-tests/elements/assets/custom-assets-domain.spec.json new file mode 100644 index 00000000..6dc197f4 --- /dev/null +++ b/test/browser/isolated-tests/elements/assets/custom-assets-domain.spec.json @@ -0,0 +1,66 @@ +{ + "item": { + "system": { + "id": "335d17ac-b6ba-4c6a-ae31-23c1193215cb", + "collection": "default", + "name": "My article", + "codename": "my_article", + "language": "en-US", + "type": "article", + "sitemap_locations": [], + "last_modified": "2019-03-27T13:21:11.38Z", + "workflow_step": "published" + }, + "elements": { + "property1": { + "type": "asset", + "name": "Teaser image", + "value": [ + { + "name": "sources.jpg", + "type": "image/jpeg", + "size": 45376, + "description": "Description of what the asset represents.", + "url": "https://assets-us-01.kc-usercontent.com/975bf280-fd91-488c-994c-2f04416e5ee3/3e76909f-599f-4742-b472-77fd4b510e92/sources.jpg", + "width": 640, + "height": 457, + "renditions": { + "default": { + "rendition_id": "b447ca6c-8020-4e8f-be57-1d110721e535", + "preset_id": "a6d98cd5-8b2c-4e50-99c9-15192bce2490", + "width": 1280, + "height": 1024, + "query": "w=1280&h=1024&fit=clip&rect=2396,169,1280,1024" + } + } + } + ] + }, + "property2": { + "type": "asset", + "name": "Teaser image", + "value": [ + { + "name": "sources.jpg", + "type": "image/jpeg", + "size": 45376, + "description": "Description of what the asset represents.", + "url": "https://assets-us-01.kc-usercontent.com/975bf280-fd91-488c-994c-2f04416e5ee3/3e76909f-599f-4742-b472-77fd4b510e92/sources.jpg", + "width": 640, + "height": 457, + "renditions": { + "default": { + "rendition_id": "b447ca6c-8020-4e8f-be57-1d110721e535", + "preset_id": "a6d98cd5-8b2c-4e50-99c9-15192bce2490", + "width": 1280, + "height": 1024, + "query": "w=1280&h=1024&fit=clip&rect=2396,169,1280,1024" + } + } + } + ] + } + } + }, + "modular_content": {} +} \ No newline at end of file diff --git a/test/browser/isolated-tests/elements/assets/custom-assets-domain.spec.ts b/test/browser/isolated-tests/elements/assets/custom-assets-domain.spec.ts new file mode 100644 index 00000000..bab24cd6 --- /dev/null +++ b/test/browser/isolated-tests/elements/assets/custom-assets-domain.spec.ts @@ -0,0 +1,46 @@ +import { deliveryUrlHelper } from '../../../../../lib'; +import { getDeliveryClientWithJson, Movie } from '../../../setup'; +import * as responseJson from '../../fake-data/fake-warrior-response.json'; + +describe('Custom assets domain', () => { + let item: Movie; + const customDomain: string = 'https://custom.com'; + + beforeAll(async () => { + const response = await getDeliveryClientWithJson(responseJson, { + assetsDomain: customDomain, + projectId: 'x' + }) + .item('xx') + .toPromise(); + + item = response.data.item; + + console.log(item); + }); + + it(`Custom asset domain should be set in asset element`, () => { + const assetElement = item.elements.poster.value[0]; + + expect(assetElement.url).toEqual( + `${customDomain}/da5abe9f-fdad-4168-97cd-b3464be2ccb9/22504ba8-2075-48fa-9d4f-8fce3de1754a/warrior.jpg` + ); + }); + + it(`Custom asset domain should be set in Rich Text element`, () => { + const richTextElement = item.elements.plot; + + for (const image of richTextElement.images) { + const imagePathname = deliveryUrlHelper.getUrlPathname(image.url); + expect(image.url).toEqual(`${customDomain}${imagePathname}`); + } + }); + + it(`Custom asset domain should be set in HTML of Rich Text elements`, () => { + const richTextElement = item.elements.plot; + + for (const image of richTextElement.images) { + expect(richTextElement.value).toContain(image.url); + } + }); +}); From b283fdfe7c1dcf18bc0b8fe97f8a329365c227ba Mon Sep 17 00:00:00 2001 From: Richard Sustek Date: Sun, 12 Dec 2021 15:56:55 +0100 Subject: [PATCH 2/3] fixes utility import path --- lib/mappers/element.mapper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mappers/element.mapper.ts b/lib/mappers/element.mapper.ts index 71fb8a41..0dc306e4 100644 --- a/lib/mappers/element.mapper.ts +++ b/lib/mappers/element.mapper.ts @@ -1,5 +1,5 @@ import { enumHelper } from '@kentico/kontent-core'; -import { deliveryUrlHelper } from '../../lib/utilities'; +import { deliveryUrlHelper } from '../utilities'; import { IDeliveryClientConfig } from '../config'; import { Contracts } from '../contracts'; From 361d62d4c832034a36013e03fa7833f45e4cfa7a Mon Sep 17 00:00:00 2001 From: Richard Sustek Date: Sun, 12 Dec 2021 16:25:28 +0100 Subject: [PATCH 3/3] docs: Adds 'assetsDomain' configuration option to readme --- readme.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index afd4a1c0..3147ab0b 100644 --- a/readme.md +++ b/readme.md @@ -156,7 +156,8 @@ Following is a list of configuration options for DeliveryClient (`IDeliveryClien | globalHeaders? | (queryConfig: IQueryConfig) => IHeader[] | Adds ability to add extra headers to each http request | | retryStrategy? | IRetryStrategyOptions | Retry strategy configuration | | linkedItemsReferenceHandler? | LinkedItemsReferenceHandler | Indicates if content items are automatically mapped. Available values: 'map' or 'ignore' | -| propertyNameResolver? | PropertyNameResolver | Used to map properties. Choose one of following default resolvers: `snakeCasePropertyNameResolver`, `pascalCasePropertyNameResolver` & `camelCasePropertyNameResolver` or create your own PropertyNameResolver function | +| propertyNameResolver? | PropertyNameResolver | Used to map properties. Choose one of following default resolvers: `snakeCasePropertyNameResolver`, `pascalCasePropertyNameResolver` & `camelCasePropertyNameResolver` or create your own PropertyNameResolver function | +| assetsDomain? | string | Custom domain for assets. Changes url of assets in both asset & rich text elements | ### Create typed models