@volvo-cars/content-delivery-client
Advanced tools
Comparing version 0.5.0 to 0.6.0
@@ -5,2 +5,3 @@ /// <reference types="node" /> | ||
import type { ContentType, ListEntriesResponseData } from './entries/types'; | ||
import { SitemapListResponseData } from './sitemap/types'; | ||
export type ContentEnvironment = 'live' | 'authoringPreview' | 'master'; | ||
@@ -46,3 +47,6 @@ export type ContentDeliveryDataSource = 'volvo-test' | 'volvo-qa' | 'volvo-prod' | string; | ||
*/ | ||
apiKey: string; | ||
apiKey: string | { | ||
qa: string; | ||
prod: string; | ||
}; | ||
/** | ||
@@ -60,2 +64,3 @@ * Path where dictionaries and other nececcary files can be written relative to your project root. | ||
listEntries?: number; | ||
sitemap?: number; | ||
}; | ||
@@ -69,2 +74,6 @@ /** | ||
} & Record<string, any>; | ||
/** | ||
* Allow the consumer to catch and log the error themselves | ||
*/ | ||
disableLogging?: boolean; | ||
}; | ||
@@ -112,2 +121,6 @@ export type CreateClientOptions = ClientConfig & { | ||
dataSource?: ContentDeliveryDataSource; | ||
/** | ||
* ContentType Id to fetch entries from Contentstack datasource | ||
*/ | ||
contentTypeId?: string; | ||
}; | ||
@@ -120,2 +133,3 @@ export type GetAllDictionariesOptions = GetOptions & { | ||
}; | ||
export type GetSitemapListOptions = Omit<GetOptions, 'locale' | 'environment' | 'market'>; | ||
export interface ContentDeliveryClient { | ||
@@ -152,3 +166,4 @@ /** | ||
listEntries<T extends ContentType>(contentType: T, options: GetOptions): Promise<ListEntriesResponseData<T>>; | ||
getSitemapList(options?: GetSitemapListOptions): Promise<SitemapListResponseData>; | ||
} | ||
export declare function createClient({ forceLocalData, fallbackToLocalData, ...config }: CreateClientOptions): ContentDeliveryClient; |
@@ -10,3 +10,5 @@ export type ContentType = 'ContentType' | 'EditorialComponent' | 'PageType'; | ||
sysId?: string; | ||
createdAt?: string; | ||
updatedAt?: string; | ||
}; | ||
} & Record<string, any>)[]; |
@@ -26,3 +26,3 @@ import type { ContentEnvironment } from './ContentDeliveryClient'; | ||
applicationId: string; | ||
locale: string; | ||
locale?: string; | ||
dataSource?: string; | ||
@@ -32,2 +32,3 @@ environment?: ContentEnvironment; | ||
operationId?: string; | ||
status: number; | ||
constructor(message: string, { applicationId, dataSource, locale, environment, operationId, market, }: RequestDetails, error?: Error[] | (Error & { | ||
@@ -34,0 +35,0 @@ errors?: Error[]; |
import flatten from 'flat'; | ||
import { getMarketSite, getMarketSiteByLocale } from '@volvo-cars/market-sites'; | ||
const canonicalDictionaryNameRegex = /^[A-Za-z0-9]+\.[A-Za-z0-9]+$/; | ||
const canonicalDictionaryNameRegex = /^[A-Za-z0-9]+\.[A-Za-z0-9_]+$/; | ||
function validateCanonicalDictionaryName(canonicalDictionaryName) { | ||
@@ -50,3 +50,3 @@ if (!canonicalDictionaryNameRegex.test(canonicalDictionaryName)) { | ||
getLocalDictionary | ||
} = await import('./getLocalDictionaries-687d3c8d.js'); | ||
} = await import('./getLocalDictionaries-d7bfaf0f.js'); | ||
return this.flattenDictionaries({ | ||
@@ -60,3 +60,3 @@ [canonicalDictionaryName]: await getLocalDictionary(canonicalDictionaryName, (_this$config$path = this.config.path) != null ? _this$config$path : '') | ||
getLocalDictionaries | ||
} = await import('./getLocalDictionaries-687d3c8d.js'); | ||
} = await import('./getLocalDictionaries-d7bfaf0f.js'); | ||
canonicalDictionaryNames.forEach(validateCanonicalDictionaryName); | ||
@@ -69,3 +69,3 @@ return this.flattenDictionaries(Object.fromEntries(await getLocalDictionaries(canonicalDictionaryNames, (_this$config$path2 = this.config.path) != null ? _this$config$path2 : ''))); | ||
getLocalDictionaries | ||
} = await import('./getLocalDictionaries-687d3c8d.js'); | ||
} = await import('./getLocalDictionaries-d7bfaf0f.js'); | ||
return this.flattenDictionaries(Object.fromEntries(await getLocalDictionaries([], (_this$config$path3 = this.config.path) != null ? _this$config$path3 : ''))); | ||
@@ -77,3 +77,3 @@ } | ||
getLocalEntry | ||
} = await import('./getLocalEntry-98f1ae6f.js'); | ||
} = await import('./getLocalEntry-bdb7cfb2.js'); | ||
return getLocalEntry(_canonicalName, (_this$config$path4 = this.config.path) != null ? _this$config$path4 : ''); | ||
@@ -84,2 +84,5 @@ } | ||
} | ||
async getSitemapList(_options) { | ||
return []; | ||
} | ||
async flattenDictionaries(target) { | ||
@@ -132,3 +135,4 @@ const { | ||
}, error) { | ||
super(`${message} in ${locale} @ ${dataSource} '${environment || 'live'}'`); | ||
var _this$errors$; | ||
super(`${message}${locale ? ` in ${locale}` : ''} @ ${dataSource} '${environment || 'live'}'`); | ||
this.name = 'ContentDeliveryError'; | ||
@@ -142,2 +146,3 @@ this.errors = []; | ||
this.operationId = void 0; | ||
this.status = 500; | ||
this.applicationId = applicationId; | ||
@@ -160,2 +165,3 @@ this.dataSource = dataSource; | ||
} | ||
this.status = ((_this$errors$ = this.errors[0]) == null ? void 0 : _this$errors$.status) || 500; | ||
} | ||
@@ -222,24 +228,42 @@ } | ||
const BASE_API_URL = 'https://gw.consumer.api.volvocars.com/content-delivery/v1/applications'; | ||
async function sendRequest({ | ||
applicationId, | ||
dataSource, | ||
path, | ||
market = false, | ||
locale, | ||
accept, | ||
environment, | ||
operationId, | ||
contentType, | ||
timeout, | ||
config, | ||
limit | ||
}, { | ||
const BASE_API_URL_PROD = 'https://gw.consumer.api.volvocars.com/content-delivery/v1/applications'; | ||
const BASE_API_URL_QA = 'https://gw.qa.consumer.api.volvocars.com/content-delivery/v1/applications'; | ||
const sendRequest = async (options, { | ||
retry = true | ||
} = {}) { | ||
} = {}) => { | ||
var _fetchOptions; | ||
const { | ||
applicationId, | ||
dataSource, | ||
path, | ||
market = false, | ||
locale, | ||
accept, | ||
acceptHeaderType = 'contentdelivery', | ||
environment, | ||
operationId, | ||
contentType, | ||
contentTypeId, | ||
timeout, | ||
config, | ||
limit, | ||
baseUrl | ||
} = options; | ||
const timer = new Timeout(timeout); | ||
const url = new URL(BASE_API_URL); | ||
const isProdDataSource = !(dataSource != null && dataSource.endsWith('-qa') || dataSource != null && dataSource.endsWith('-test')); | ||
const optInEnvSpecificUrl = typeof config.apiKey === 'object'; | ||
const defaultApiUrl = optInEnvSpecificUrl ? isProdDataSource ? BASE_API_URL_PROD : BASE_API_URL_QA : BASE_API_URL_PROD; | ||
const url = new URL(baseUrl || defaultApiUrl); | ||
const apiKeyEnv = isProdDataSource ? 'prod' : 'qa'; | ||
const apiKey = typeof config.apiKey === 'string' ? config.apiKey : config.apiKey[apiKeyEnv]; | ||
if (url.origin.startsWith('https://gw.consumer.api.volvocars.com') && !isProdDataSource && !(dataSource != null && dataSource.startsWith('dotcom-sitecore-'))) { | ||
console.log(`[Warning: ContentDeliveryClient] QA and TEST dataSources will soon only work with | ||
QA consumer portal(https://gw.qa.consumer.api.volvocars.com), make sure to change "apiKey" | ||
config option from a string to an object with different values for each environment: | ||
detected [${dataSource}] with production consumer portal URL`); | ||
} | ||
url.pathname += `/${applicationId}${path}`; | ||
url.searchParams.set('Locale', locale); | ||
if (locale) { | ||
url.searchParams.set('Locale', locale); | ||
} | ||
if (dataSource) { | ||
@@ -253,3 +277,3 @@ url.searchParams.set('DataSource', dataSource); | ||
url.searchParams.set('environment', environment); | ||
if (market) { | ||
if (market && locale) { | ||
const { | ||
@@ -266,2 +290,10 @@ siteSlug | ||
} | ||
if (contentTypeId) { | ||
var _dataSource$toLowerCa; | ||
if (dataSource != null && (_dataSource$toLowerCa = dataSource.toLowerCase()) != null && _dataSource$toLowerCa.startsWith('contentstack')) { | ||
url.searchParams.set('contentTypeId', contentTypeId); | ||
} else { | ||
throw new Error(`The 'contentTypeId' option is only supported for Contentstack datasource`); | ||
} | ||
} | ||
if (limit) { | ||
@@ -277,18 +309,17 @@ url.searchParams.set('limit', limit.toString()); | ||
} | ||
const response = await Promise.race([timer.run(), fetch(url.href, { | ||
headers: { | ||
Accept: `application/vnd.volvocars.api.contentdelivery.${accept}+json`, | ||
'VCC-Api-Key': config.apiKey, | ||
...(operationId && { | ||
'VCC-Api-OperationId': operationId | ||
}) | ||
}, | ||
...fetchOptions | ||
}).then(response => { | ||
let response; | ||
try { | ||
response = await Promise.race([timer.run(), fetch(url.href, { | ||
headers: { | ||
Accept: `application/vnd.volvocars.api.${acceptHeaderType}.${accept}+json`, | ||
'VCC-Api-Key': apiKey, | ||
...(operationId && { | ||
'VCC-Api-OperationId': operationId | ||
}) | ||
}, | ||
...fetchOptions | ||
})]); | ||
} finally { | ||
timer.clear(); | ||
return response; | ||
}, error => { | ||
timer.clear(); | ||
throw error; | ||
})]); | ||
} | ||
if (response.status === 429 && retry && !timeout) { | ||
@@ -300,15 +331,3 @@ const retryAfter = Number(response.headers.get('Retry-After') || 10) + 5; | ||
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000)); | ||
return sendRequest({ | ||
applicationId, | ||
dataSource, | ||
path, | ||
market, | ||
locale, | ||
accept, | ||
environment, | ||
operationId, | ||
contentType, | ||
timeout, | ||
config | ||
}, { | ||
return sendRequest(options, { | ||
retry: false | ||
@@ -323,3 +342,3 @@ }); | ||
throw new RequestError('error' in result ? result.error.message : result.message, response.status, response.url); | ||
} | ||
}; | ||
let keepAliveAgent; | ||
@@ -463,3 +482,3 @@ async function getKeepAliveAgent() { | ||
// Throw a NotFoundError to allow catching and falling back to local data | ||
throw new DictionaryNotFoundError('Empty dictionaries response', requestDetails); | ||
throw new DictionaryNotFoundError('Empty dictionaries response', requestDetails, new RequestError('Empty dictionaries response', 404, '/dictionaries')); | ||
} | ||
@@ -486,3 +505,3 @@ return results; | ||
var _obj$contentType; | ||
if ((obj == null ? void 0 : (_obj$contentType = obj.contentType) == null ? void 0 : _obj$contentType.type) === 'EditorialComponent' && key === 'fields') { | ||
if ((obj == null || (_obj$contentType = obj.contentType) == null ? void 0 : _obj$contentType.type) === 'EditorialComponent' && key === 'fields') { | ||
acc['content'] = traverse(value); | ||
@@ -496,3 +515,8 @@ } else { | ||
if (key === 'contentType' && 'sys' in obj) { | ||
value.sysId = obj.sys.id; | ||
value = { | ||
...value, | ||
sysId: obj.sys.id, | ||
createdAt: obj.sys.createdAt, | ||
updatedAt: obj.sys.updatedAt | ||
}; | ||
} | ||
@@ -508,20 +532,20 @@ acc[key] = traverse(value); | ||
function transformImageOrFile(value) { | ||
var _value$file, _value$file2, _value$file2$mediaTyp, _value$file9, _value$file10; | ||
var _value$file, _value$file2, _value$file9, _value$file10; | ||
if (!(value != null && (_value$file = value.file) != null && _value$file.url)) { | ||
return null; | ||
} | ||
if (value != null && (_value$file2 = value.file) != null && (_value$file2$mediaTyp = _value$file2.mediaType) != null && _value$file2$mediaTyp.includes('image')) { | ||
var _value$file3, _value$file4, _value$file4$details, _value$file4$details$, _value$file5, _value$file6, _value$file7, _value$file7$details, _value$file7$details$, _value$file8, _value$file8$details, _value$file8$details$; | ||
if (value != null && (_value$file2 = value.file) != null && (_value$file2 = _value$file2.mediaType) != null && _value$file2.includes('image')) { | ||
var _value$file3, _value$file4, _value$file5, _value$file6, _value$file7, _value$file8; | ||
return { | ||
src: (value == null ? void 0 : (_value$file3 = value.file) == null ? void 0 : _value$file3.url) || '', | ||
alt: (value == null ? void 0 : (_value$file4 = value.file) == null ? void 0 : (_value$file4$details = _value$file4.details) == null ? void 0 : (_value$file4$details$ = _value$file4$details.image) == null ? void 0 : _value$file4$details$.alt) || '', | ||
title: (value == null ? void 0 : value.title) || (value == null ? void 0 : (_value$file5 = value.file) == null ? void 0 : _value$file5.title) || '', | ||
description: (value == null ? void 0 : value.description) || (value == null ? void 0 : (_value$file6 = value.file) == null ? void 0 : _value$file6.description) || '', | ||
width: (value == null ? void 0 : (_value$file7 = value.file) == null ? void 0 : (_value$file7$details = _value$file7.details) == null ? void 0 : (_value$file7$details$ = _value$file7$details.image) == null ? void 0 : _value$file7$details$.width) || 0, | ||
height: (value == null ? void 0 : (_value$file8 = value.file) == null ? void 0 : (_value$file8$details = _value$file8.details) == null ? void 0 : (_value$file8$details$ = _value$file8$details.image) == null ? void 0 : _value$file8$details$.height) || 0 | ||
src: (value == null || (_value$file3 = value.file) == null ? void 0 : _value$file3.url) || '', | ||
alt: (value == null || (_value$file4 = value.file) == null || (_value$file4 = _value$file4.details) == null || (_value$file4 = _value$file4.image) == null ? void 0 : _value$file4.alt) || '', | ||
title: (value == null ? void 0 : value.title) || (value == null || (_value$file5 = value.file) == null ? void 0 : _value$file5.title) || '', | ||
description: (value == null ? void 0 : value.description) || (value == null || (_value$file6 = value.file) == null ? void 0 : _value$file6.description) || '', | ||
width: (value == null || (_value$file7 = value.file) == null || (_value$file7 = _value$file7.details) == null || (_value$file7 = _value$file7.image) == null ? void 0 : _value$file7.width) || 0, | ||
height: (value == null || (_value$file8 = value.file) == null || (_value$file8 = _value$file8.details) == null || (_value$file8 = _value$file8.image) == null ? void 0 : _value$file8.height) || 0 | ||
}; | ||
} | ||
return { | ||
mimeType: (value == null ? void 0 : (_value$file9 = value.file) == null ? void 0 : _value$file9.mediaType) || '', | ||
src: (value == null ? void 0 : (_value$file10 = value.file) == null ? void 0 : _value$file10.url) || '', | ||
mimeType: (value == null || (_value$file9 = value.file) == null ? void 0 : _value$file9.mediaType) || '', | ||
src: (value == null || (_value$file10 = value.file) == null ? void 0 : _value$file10.url) || '', | ||
title: (value == null ? void 0 : value.title) || '', | ||
@@ -532,3 +556,3 @@ description: (value == null ? void 0 : value.description) || '' | ||
function handleBooleans(value) { | ||
return value === 'true' ? true : value === 'false' ? false : typeof value === 'undefined' ? null : value; | ||
return value === 'true' || value === 'True' ? true : value === 'false' || value === 'False' ? false : typeof value === 'undefined' ? null : value; | ||
} | ||
@@ -545,3 +569,4 @@ | ||
market: options.market, | ||
timeout: options.timeout | ||
timeout: options.timeout, | ||
contentTypeId: options.contentTypeId | ||
}; | ||
@@ -578,3 +603,4 @@ let result; | ||
timeout: options.timeout, | ||
contentType | ||
contentType, | ||
contentTypeId: options.contentTypeId | ||
}; | ||
@@ -600,2 +626,35 @@ try { | ||
const SITEMAP_BASE_API_URL = 'https://gw.consumer.api.volvocars.com/sitemap/v1/applications'; | ||
async function getSitemapList(config, options) { | ||
var _options$environment; | ||
const requestDetails = { | ||
applicationId: config.applicationId, | ||
dataSource: options.dataSource, | ||
operationId: options.operationId, | ||
locale: options.locale, | ||
environment: (_options$environment = options.environment) != null ? _options$environment : 'live', | ||
market: options.market, | ||
timeout: options.timeout | ||
}; | ||
try { | ||
const result = await sendRequest({ | ||
...requestDetails, | ||
config, | ||
path: `/sitemap`, | ||
accept: 'sitemapitemlistsuccessresponse', | ||
acceptHeaderType: 'sitemapservice', | ||
baseUrl: SITEMAP_BASE_API_URL | ||
}); | ||
return result; | ||
} catch (error) { | ||
let message = 'Failed fetching sitemap list'; | ||
if (error instanceof RequestError) { | ||
message += ` '${error.status}: ${error.message}'`; | ||
} else if (error.message) { | ||
message += ` (${error.message})`; | ||
} | ||
throw new ContentDeliveryError(message, requestDetails, error); | ||
} | ||
} | ||
class Cache { | ||
@@ -673,2 +732,3 @@ /** | ||
this.listEntriesCache = void 0; | ||
this.sitemapCache = void 0; | ||
this.dictionariesListCache = void 0; | ||
@@ -698,3 +758,4 @@ this.localDataClient = void 0; | ||
entries: revalidateDefault, | ||
listEntries: revalidateDefault | ||
listEntries: revalidateDefault, | ||
sitemap: revalidateDefault | ||
}; | ||
@@ -717,2 +778,3 @@ if (typeof config.revalidate === 'object') { | ||
this.listEntriesCache = new Cache(this.config.revalidate.listEntries); | ||
this.sitemapCache = new Cache(this.config.revalidate.sitemap); | ||
} | ||
@@ -1052,2 +1114,43 @@ get isValidating() { | ||
} | ||
async getSitemapList(rawOptions) { | ||
const options = getOptionsWithDefaults({ | ||
locale: '', | ||
...rawOptions | ||
}, this.config); | ||
validateDataSourceAndEnvironment(this.config, options); | ||
const operationDetails = { | ||
environment: options.environment, | ||
dataSource: options.dataSource, | ||
locale: '' | ||
}; | ||
const cacheKey = { | ||
...operationDetails, | ||
name: 'sitemap' | ||
}; | ||
if (this.config.revalidate.sitemap === 0 || !this.sitemapCache.has(cacheKey)) { | ||
try { | ||
const sitemap = await getSitemapList(this.config, options); | ||
if (this.sitemapCache) { | ||
this.sitemapCache.set(cacheKey, sitemap); | ||
} | ||
return sitemap; | ||
} catch (error) { | ||
this.logError('sitemap', operationDetails, error); | ||
throw error; | ||
} | ||
} | ||
if (this.sitemapCache.isStale(cacheKey)) { | ||
this.pendingRevalidations++; | ||
getSitemapList(this.config, options).then(sitemap => { | ||
this.sitemapCache.set(cacheKey, sitemap); | ||
}).catch(error => { | ||
// Failing to refresh a stale sitemap list is not a fatal error, because | ||
// there's already some content in the cache. | ||
this.logError('sitemap', operationDetails, error); | ||
}).finally(() => { | ||
this.pendingRevalidations--; | ||
}); | ||
} | ||
return this.sitemapCache.get(cacheKey); | ||
} | ||
logError(type, { | ||
@@ -1058,3 +1161,3 @@ locale, | ||
}, error) { | ||
if (process.env.NODE_ENV !== 'test') { | ||
if (process.env.NODE_ENV !== 'test' && !this.config.disableLogging) { | ||
console.log(`Failed fetching ${type} for locale '${locale}' in dataSource '${dataSource} @ ${environment}'.`); | ||
@@ -1065,3 +1168,3 @@ console.error(getMessages(error)); | ||
logFallback() { | ||
if (process.env.NODE_ENV !== 'test') { | ||
if (process.env.NODE_ENV !== 'test' && !this.config.disableLogging) { | ||
console.log('Falling back to local data...'); | ||
@@ -1068,0 +1171,0 @@ } |
@@ -26,3 +26,3 @@ var flatten = require('flat'); | ||
const canonicalDictionaryNameRegex = /^[A-Za-z0-9]+\.[A-Za-z0-9]+$/; | ||
const canonicalDictionaryNameRegex = /^[A-Za-z0-9]+\.[A-Za-z0-9_]+$/; | ||
function validateCanonicalDictionaryName(canonicalDictionaryName) { | ||
@@ -73,3 +73,3 @@ if (!canonicalDictionaryNameRegex.test(canonicalDictionaryName)) { | ||
getLocalDictionary | ||
} = await Promise.resolve().then(function () { return require('./getLocalDictionaries-c67434bc.js'); }); | ||
} = await Promise.resolve().then(function () { return require('./getLocalDictionaries-cf79408b.js'); }); | ||
return this.flattenDictionaries({ | ||
@@ -83,3 +83,3 @@ [canonicalDictionaryName]: await getLocalDictionary(canonicalDictionaryName, (_this$config$path = this.config.path) != null ? _this$config$path : '') | ||
getLocalDictionaries | ||
} = await Promise.resolve().then(function () { return require('./getLocalDictionaries-c67434bc.js'); }); | ||
} = await Promise.resolve().then(function () { return require('./getLocalDictionaries-cf79408b.js'); }); | ||
canonicalDictionaryNames.forEach(validateCanonicalDictionaryName); | ||
@@ -92,3 +92,3 @@ return this.flattenDictionaries(Object.fromEntries(await getLocalDictionaries(canonicalDictionaryNames, (_this$config$path2 = this.config.path) != null ? _this$config$path2 : ''))); | ||
getLocalDictionaries | ||
} = await Promise.resolve().then(function () { return require('./getLocalDictionaries-c67434bc.js'); }); | ||
} = await Promise.resolve().then(function () { return require('./getLocalDictionaries-cf79408b.js'); }); | ||
return this.flattenDictionaries(Object.fromEntries(await getLocalDictionaries([], (_this$config$path3 = this.config.path) != null ? _this$config$path3 : ''))); | ||
@@ -100,3 +100,3 @@ } | ||
getLocalEntry | ||
} = await Promise.resolve().then(function () { return require('./getLocalEntry-d8c829fa.js'); }); | ||
} = await Promise.resolve().then(function () { return require('./getLocalEntry-718c5d08.js'); }); | ||
return getLocalEntry(_canonicalName, (_this$config$path4 = this.config.path) != null ? _this$config$path4 : ''); | ||
@@ -107,2 +107,5 @@ } | ||
} | ||
async getSitemapList(_options) { | ||
return []; | ||
} | ||
async flattenDictionaries(target) { | ||
@@ -155,3 +158,4 @@ const { | ||
}, error) { | ||
super(`${message} in ${locale} @ ${dataSource} '${environment || 'live'}'`); | ||
var _this$errors$; | ||
super(`${message}${locale ? ` in ${locale}` : ''} @ ${dataSource} '${environment || 'live'}'`); | ||
this.name = 'ContentDeliveryError'; | ||
@@ -165,2 +169,3 @@ this.errors = []; | ||
this.operationId = void 0; | ||
this.status = 500; | ||
this.applicationId = applicationId; | ||
@@ -183,2 +188,3 @@ this.dataSource = dataSource; | ||
} | ||
this.status = ((_this$errors$ = this.errors[0]) == null ? void 0 : _this$errors$.status) || 500; | ||
} | ||
@@ -245,24 +251,42 @@ } | ||
const BASE_API_URL = 'https://gw.consumer.api.volvocars.com/content-delivery/v1/applications'; | ||
async function sendRequest({ | ||
applicationId, | ||
dataSource, | ||
path, | ||
market = false, | ||
locale, | ||
accept, | ||
environment, | ||
operationId, | ||
contentType, | ||
timeout, | ||
config, | ||
limit | ||
}, { | ||
const BASE_API_URL_PROD = 'https://gw.consumer.api.volvocars.com/content-delivery/v1/applications'; | ||
const BASE_API_URL_QA = 'https://gw.qa.consumer.api.volvocars.com/content-delivery/v1/applications'; | ||
const sendRequest = async (options, { | ||
retry = true | ||
} = {}) { | ||
} = {}) => { | ||
var _fetchOptions; | ||
const { | ||
applicationId, | ||
dataSource, | ||
path, | ||
market = false, | ||
locale, | ||
accept, | ||
acceptHeaderType = 'contentdelivery', | ||
environment, | ||
operationId, | ||
contentType, | ||
contentTypeId, | ||
timeout, | ||
config, | ||
limit, | ||
baseUrl | ||
} = options; | ||
const timer = new Timeout(timeout); | ||
const url = new URL(BASE_API_URL); | ||
const isProdDataSource = !(dataSource != null && dataSource.endsWith('-qa') || dataSource != null && dataSource.endsWith('-test')); | ||
const optInEnvSpecificUrl = typeof config.apiKey === 'object'; | ||
const defaultApiUrl = optInEnvSpecificUrl ? isProdDataSource ? BASE_API_URL_PROD : BASE_API_URL_QA : BASE_API_URL_PROD; | ||
const url = new URL(baseUrl || defaultApiUrl); | ||
const apiKeyEnv = isProdDataSource ? 'prod' : 'qa'; | ||
const apiKey = typeof config.apiKey === 'string' ? config.apiKey : config.apiKey[apiKeyEnv]; | ||
if (url.origin.startsWith('https://gw.consumer.api.volvocars.com') && !isProdDataSource && !(dataSource != null && dataSource.startsWith('dotcom-sitecore-'))) { | ||
console.log(`[Warning: ContentDeliveryClient] QA and TEST dataSources will soon only work with | ||
QA consumer portal(https://gw.qa.consumer.api.volvocars.com), make sure to change "apiKey" | ||
config option from a string to an object with different values for each environment: | ||
detected [${dataSource}] with production consumer portal URL`); | ||
} | ||
url.pathname += `/${applicationId}${path}`; | ||
url.searchParams.set('Locale', locale); | ||
if (locale) { | ||
url.searchParams.set('Locale', locale); | ||
} | ||
if (dataSource) { | ||
@@ -276,3 +300,3 @@ url.searchParams.set('DataSource', dataSource); | ||
url.searchParams.set('environment', environment); | ||
if (market) { | ||
if (market && locale) { | ||
const { | ||
@@ -289,2 +313,10 @@ siteSlug | ||
} | ||
if (contentTypeId) { | ||
var _dataSource$toLowerCa; | ||
if (dataSource != null && (_dataSource$toLowerCa = dataSource.toLowerCase()) != null && _dataSource$toLowerCa.startsWith('contentstack')) { | ||
url.searchParams.set('contentTypeId', contentTypeId); | ||
} else { | ||
throw new Error(`The 'contentTypeId' option is only supported for Contentstack datasource`); | ||
} | ||
} | ||
if (limit) { | ||
@@ -300,18 +332,17 @@ url.searchParams.set('limit', limit.toString()); | ||
} | ||
const response = await Promise.race([timer.run(), fetch(url.href, { | ||
headers: { | ||
Accept: `application/vnd.volvocars.api.contentdelivery.${accept}+json`, | ||
'VCC-Api-Key': config.apiKey, | ||
...(operationId && { | ||
'VCC-Api-OperationId': operationId | ||
}) | ||
}, | ||
...fetchOptions | ||
}).then(response => { | ||
let response; | ||
try { | ||
response = await Promise.race([timer.run(), fetch(url.href, { | ||
headers: { | ||
Accept: `application/vnd.volvocars.api.${acceptHeaderType}.${accept}+json`, | ||
'VCC-Api-Key': apiKey, | ||
...(operationId && { | ||
'VCC-Api-OperationId': operationId | ||
}) | ||
}, | ||
...fetchOptions | ||
})]); | ||
} finally { | ||
timer.clear(); | ||
return response; | ||
}, error => { | ||
timer.clear(); | ||
throw error; | ||
})]); | ||
} | ||
if (response.status === 429 && retry && !timeout) { | ||
@@ -323,15 +354,3 @@ const retryAfter = Number(response.headers.get('Retry-After') || 10) + 5; | ||
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000)); | ||
return sendRequest({ | ||
applicationId, | ||
dataSource, | ||
path, | ||
market, | ||
locale, | ||
accept, | ||
environment, | ||
operationId, | ||
contentType, | ||
timeout, | ||
config | ||
}, { | ||
return sendRequest(options, { | ||
retry: false | ||
@@ -346,3 +365,3 @@ }); | ||
throw new RequestError('error' in result ? result.error.message : result.message, response.status, response.url); | ||
} | ||
}; | ||
let keepAliveAgent; | ||
@@ -486,3 +505,3 @@ async function getKeepAliveAgent() { | ||
// Throw a NotFoundError to allow catching and falling back to local data | ||
throw new DictionaryNotFoundError('Empty dictionaries response', requestDetails); | ||
throw new DictionaryNotFoundError('Empty dictionaries response', requestDetails, new RequestError('Empty dictionaries response', 404, '/dictionaries')); | ||
} | ||
@@ -509,3 +528,3 @@ return results; | ||
var _obj$contentType; | ||
if ((obj == null ? void 0 : (_obj$contentType = obj.contentType) == null ? void 0 : _obj$contentType.type) === 'EditorialComponent' && key === 'fields') { | ||
if ((obj == null || (_obj$contentType = obj.contentType) == null ? void 0 : _obj$contentType.type) === 'EditorialComponent' && key === 'fields') { | ||
acc['content'] = traverse(value); | ||
@@ -519,3 +538,8 @@ } else { | ||
if (key === 'contentType' && 'sys' in obj) { | ||
value.sysId = obj.sys.id; | ||
value = { | ||
...value, | ||
sysId: obj.sys.id, | ||
createdAt: obj.sys.createdAt, | ||
updatedAt: obj.sys.updatedAt | ||
}; | ||
} | ||
@@ -531,20 +555,20 @@ acc[key] = traverse(value); | ||
function transformImageOrFile(value) { | ||
var _value$file, _value$file2, _value$file2$mediaTyp, _value$file9, _value$file10; | ||
var _value$file, _value$file2, _value$file9, _value$file10; | ||
if (!(value != null && (_value$file = value.file) != null && _value$file.url)) { | ||
return null; | ||
} | ||
if (value != null && (_value$file2 = value.file) != null && (_value$file2$mediaTyp = _value$file2.mediaType) != null && _value$file2$mediaTyp.includes('image')) { | ||
var _value$file3, _value$file4, _value$file4$details, _value$file4$details$, _value$file5, _value$file6, _value$file7, _value$file7$details, _value$file7$details$, _value$file8, _value$file8$details, _value$file8$details$; | ||
if (value != null && (_value$file2 = value.file) != null && (_value$file2 = _value$file2.mediaType) != null && _value$file2.includes('image')) { | ||
var _value$file3, _value$file4, _value$file5, _value$file6, _value$file7, _value$file8; | ||
return { | ||
src: (value == null ? void 0 : (_value$file3 = value.file) == null ? void 0 : _value$file3.url) || '', | ||
alt: (value == null ? void 0 : (_value$file4 = value.file) == null ? void 0 : (_value$file4$details = _value$file4.details) == null ? void 0 : (_value$file4$details$ = _value$file4$details.image) == null ? void 0 : _value$file4$details$.alt) || '', | ||
title: (value == null ? void 0 : value.title) || (value == null ? void 0 : (_value$file5 = value.file) == null ? void 0 : _value$file5.title) || '', | ||
description: (value == null ? void 0 : value.description) || (value == null ? void 0 : (_value$file6 = value.file) == null ? void 0 : _value$file6.description) || '', | ||
width: (value == null ? void 0 : (_value$file7 = value.file) == null ? void 0 : (_value$file7$details = _value$file7.details) == null ? void 0 : (_value$file7$details$ = _value$file7$details.image) == null ? void 0 : _value$file7$details$.width) || 0, | ||
height: (value == null ? void 0 : (_value$file8 = value.file) == null ? void 0 : (_value$file8$details = _value$file8.details) == null ? void 0 : (_value$file8$details$ = _value$file8$details.image) == null ? void 0 : _value$file8$details$.height) || 0 | ||
src: (value == null || (_value$file3 = value.file) == null ? void 0 : _value$file3.url) || '', | ||
alt: (value == null || (_value$file4 = value.file) == null || (_value$file4 = _value$file4.details) == null || (_value$file4 = _value$file4.image) == null ? void 0 : _value$file4.alt) || '', | ||
title: (value == null ? void 0 : value.title) || (value == null || (_value$file5 = value.file) == null ? void 0 : _value$file5.title) || '', | ||
description: (value == null ? void 0 : value.description) || (value == null || (_value$file6 = value.file) == null ? void 0 : _value$file6.description) || '', | ||
width: (value == null || (_value$file7 = value.file) == null || (_value$file7 = _value$file7.details) == null || (_value$file7 = _value$file7.image) == null ? void 0 : _value$file7.width) || 0, | ||
height: (value == null || (_value$file8 = value.file) == null || (_value$file8 = _value$file8.details) == null || (_value$file8 = _value$file8.image) == null ? void 0 : _value$file8.height) || 0 | ||
}; | ||
} | ||
return { | ||
mimeType: (value == null ? void 0 : (_value$file9 = value.file) == null ? void 0 : _value$file9.mediaType) || '', | ||
src: (value == null ? void 0 : (_value$file10 = value.file) == null ? void 0 : _value$file10.url) || '', | ||
mimeType: (value == null || (_value$file9 = value.file) == null ? void 0 : _value$file9.mediaType) || '', | ||
src: (value == null || (_value$file10 = value.file) == null ? void 0 : _value$file10.url) || '', | ||
title: (value == null ? void 0 : value.title) || '', | ||
@@ -555,3 +579,3 @@ description: (value == null ? void 0 : value.description) || '' | ||
function handleBooleans(value) { | ||
return value === 'true' ? true : value === 'false' ? false : typeof value === 'undefined' ? null : value; | ||
return value === 'true' || value === 'True' ? true : value === 'false' || value === 'False' ? false : typeof value === 'undefined' ? null : value; | ||
} | ||
@@ -568,3 +592,4 @@ | ||
market: options.market, | ||
timeout: options.timeout | ||
timeout: options.timeout, | ||
contentTypeId: options.contentTypeId | ||
}; | ||
@@ -601,3 +626,4 @@ let result; | ||
timeout: options.timeout, | ||
contentType | ||
contentType, | ||
contentTypeId: options.contentTypeId | ||
}; | ||
@@ -623,2 +649,35 @@ try { | ||
const SITEMAP_BASE_API_URL = 'https://gw.consumer.api.volvocars.com/sitemap/v1/applications'; | ||
async function getSitemapList(config, options) { | ||
var _options$environment; | ||
const requestDetails = { | ||
applicationId: config.applicationId, | ||
dataSource: options.dataSource, | ||
operationId: options.operationId, | ||
locale: options.locale, | ||
environment: (_options$environment = options.environment) != null ? _options$environment : 'live', | ||
market: options.market, | ||
timeout: options.timeout | ||
}; | ||
try { | ||
const result = await sendRequest({ | ||
...requestDetails, | ||
config, | ||
path: `/sitemap`, | ||
accept: 'sitemapitemlistsuccessresponse', | ||
acceptHeaderType: 'sitemapservice', | ||
baseUrl: SITEMAP_BASE_API_URL | ||
}); | ||
return result; | ||
} catch (error) { | ||
let message = 'Failed fetching sitemap list'; | ||
if (error instanceof RequestError) { | ||
message += ` '${error.status}: ${error.message}'`; | ||
} else if (error.message) { | ||
message += ` (${error.message})`; | ||
} | ||
throw new ContentDeliveryError(message, requestDetails, error); | ||
} | ||
} | ||
class Cache { | ||
@@ -696,2 +755,3 @@ /** | ||
this.listEntriesCache = void 0; | ||
this.sitemapCache = void 0; | ||
this.dictionariesListCache = void 0; | ||
@@ -721,3 +781,4 @@ this.localDataClient = void 0; | ||
entries: revalidateDefault, | ||
listEntries: revalidateDefault | ||
listEntries: revalidateDefault, | ||
sitemap: revalidateDefault | ||
}; | ||
@@ -740,2 +801,3 @@ if (typeof config.revalidate === 'object') { | ||
this.listEntriesCache = new Cache(this.config.revalidate.listEntries); | ||
this.sitemapCache = new Cache(this.config.revalidate.sitemap); | ||
} | ||
@@ -1075,2 +1137,43 @@ get isValidating() { | ||
} | ||
async getSitemapList(rawOptions) { | ||
const options = getOptionsWithDefaults({ | ||
locale: '', | ||
...rawOptions | ||
}, this.config); | ||
validateDataSourceAndEnvironment(this.config, options); | ||
const operationDetails = { | ||
environment: options.environment, | ||
dataSource: options.dataSource, | ||
locale: '' | ||
}; | ||
const cacheKey = { | ||
...operationDetails, | ||
name: 'sitemap' | ||
}; | ||
if (this.config.revalidate.sitemap === 0 || !this.sitemapCache.has(cacheKey)) { | ||
try { | ||
const sitemap = await getSitemapList(this.config, options); | ||
if (this.sitemapCache) { | ||
this.sitemapCache.set(cacheKey, sitemap); | ||
} | ||
return sitemap; | ||
} catch (error) { | ||
this.logError('sitemap', operationDetails, error); | ||
throw error; | ||
} | ||
} | ||
if (this.sitemapCache.isStale(cacheKey)) { | ||
this.pendingRevalidations++; | ||
getSitemapList(this.config, options).then(sitemap => { | ||
this.sitemapCache.set(cacheKey, sitemap); | ||
}).catch(error => { | ||
// Failing to refresh a stale sitemap list is not a fatal error, because | ||
// there's already some content in the cache. | ||
this.logError('sitemap', operationDetails, error); | ||
}).finally(() => { | ||
this.pendingRevalidations--; | ||
}); | ||
} | ||
return this.sitemapCache.get(cacheKey); | ||
} | ||
logError(type, { | ||
@@ -1081,3 +1184,3 @@ locale, | ||
}, error) { | ||
if (process.env.NODE_ENV !== 'test') { | ||
if (process.env.NODE_ENV !== 'test' && !this.config.disableLogging) { | ||
console.log(`Failed fetching ${type} for locale '${locale}' in dataSource '${dataSource} @ ${environment}'.`); | ||
@@ -1088,3 +1191,3 @@ console.error(getMessages(error)); | ||
logFallback() { | ||
if (process.env.NODE_ENV !== 'test') { | ||
if (process.env.NODE_ENV !== 'test' && !this.config.disableLogging) { | ||
console.log('Falling back to local data...'); | ||
@@ -1091,0 +1194,0 @@ } |
@@ -1,4 +0,5 @@ | ||
import type { ClientConfig, ContentDeliveryClient, GetOptions } from './ContentDeliveryClient'; | ||
import type { ClientConfig, ContentDeliveryClient, GetOptions, GetSitemapListOptions } from './ContentDeliveryClient'; | ||
import type { FlattenedDictionaries } from './dictionaries/types'; | ||
import type { ContentType, ListEntriesResponseData } from './entries/types'; | ||
import type { SitemapListResponseData } from './sitemap/types'; | ||
export declare class LocalDataClient implements ContentDeliveryClient { | ||
@@ -15,3 +16,4 @@ applicationId: string; | ||
listEntries<T extends ContentType>(_contentType: T, _options: GetOptions): Promise<ListEntriesResponseData<T>>; | ||
getSitemapList(_options?: GetSitemapListOptions): Promise<SitemapListResponseData>; | ||
private flattenDictionaries; | ||
} |
import { ClientConfig } from '.'; | ||
import { ContentDeliveryClient, ContentDeliveryDataSource, ContentEnvironment, GetAllDictionariesOptions, GetOptions } from './ContentDeliveryClient'; | ||
import { ContentDeliveryClient, ContentDeliveryDataSource, ContentEnvironment, GetAllDictionariesOptions, GetOptions, GetSitemapListOptions } from './ContentDeliveryClient'; | ||
import { FlattenedDictionaries } from './dictionaries/types'; | ||
import { ContentType, ListEntriesResponseData } from './entries/types'; | ||
import { LocalDataClient } from './LocalDataClient'; | ||
import { SitemapListResponseData } from './sitemap/types'; | ||
export type FinalClientConfig = Omit<ClientConfig, 'dataSource'> & { | ||
@@ -14,2 +15,3 @@ defaultEnvironment: ContentEnvironment; | ||
listEntries: number; | ||
sitemap: number; | ||
}; | ||
@@ -24,2 +26,3 @@ }; | ||
private listEntriesCache; | ||
private sitemapCache; | ||
private dictionariesListCache; | ||
@@ -35,4 +38,5 @@ private localDataClient?; | ||
listEntries<T extends ContentType>(contentType: T, rawOptions: GetOptions): Promise<ListEntriesResponseData<T>>; | ||
getSitemapList(rawOptions?: GetSitemapListOptions): Promise<SitemapListResponseData>; | ||
private logError; | ||
private logFallback; | ||
} |
@@ -6,4 +6,4 @@ /// <reference types="node" /> | ||
constructor(seconds?: number); | ||
run(): Promise<void>; | ||
run(): Promise<never>; | ||
clear(): void; | ||
} |
@@ -8,4 +8,5 @@ import { GetOptions } from '../../ContentDeliveryClient'; | ||
timeout?: number | undefined; | ||
contentTypeId?: string | undefined; | ||
dataSource: string | undefined; | ||
environment: import("../../ContentDeliveryClient").ContentEnvironment; | ||
}; |
@@ -5,3 +5,3 @@ import type { ClientConfig, ContentEnvironment } from '../ContentDeliveryClient'; | ||
applicationId: string; | ||
locale: string; | ||
locale?: string; | ||
environment: ContentEnvironment; | ||
@@ -12,2 +12,3 @@ dataSource?: string; | ||
contentType?: ContentType; | ||
contentTypeId?: string; | ||
timeout?: number; | ||
@@ -20,6 +21,8 @@ limit?: number; | ||
accept: string; | ||
acceptHeaderType?: string; | ||
baseUrl?: string; | ||
}; | ||
export declare function sendRequest<Results>({ applicationId, dataSource, path, market, locale, accept, environment, operationId, contentType, timeout, config, limit, }: RequestDetails & SendRequestOptions, { retry }?: { | ||
export declare const sendRequest: <Results>(options: RequestDetails & SendRequestOptions, { retry }?: { | ||
retry?: boolean | undefined; | ||
}): Promise<Results>; | ||
}) => Promise<Results>; | ||
export {}; |
{ | ||
"name": "@volvo-cars/content-delivery-client", | ||
"version": "0.5.0", | ||
"version": "0.6.0", | ||
"license": "UNLICENSED", | ||
@@ -32,5 +32,5 @@ "source": "index.ts", | ||
"@types/flat": "^5.0.1", | ||
"@volvo-cars/market-sites": "1.7.3", | ||
"@volvo-cars/market-sites": "1.8.0", | ||
"flat": "^5.0.0", | ||
"glob": "^8.1.0" | ||
"glob": "^10.3.4" | ||
}, | ||
@@ -40,8 +40,9 @@ "devDependencies": { | ||
"@vcc-www/utils": "0.0.0", | ||
"@volvo-cars/content-management-client": "0.21.0", | ||
"@volvo-cars/content-management-client": "0.22.0", | ||
"microbundle": "^0.15.1", | ||
"msw": "^1.2.1", | ||
"typescript": "5.0.2" | ||
"typescript": "5.2.2" | ||
}, | ||
"module": "dist/index.esm.js" | ||
"module": "dist/index.esm.js", | ||
"types": "dist/index.d.ts" | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
349925
53
3272
+ Added@isaacs/cliui@8.0.2(transitive)
+ Added@pkgjs/parseargs@0.11.0(transitive)
+ Added@volvo-cars/market-sites@1.8.0(transitive)
+ Addedansi-regex@5.0.16.1.0(transitive)
+ Addedansi-styles@4.3.06.2.1(transitive)
+ Addedcolor-convert@2.0.1(transitive)
+ Addedcolor-name@1.1.4(transitive)
+ Addedcross-spawn@7.0.5(transitive)
+ Addedeastasianwidth@0.2.0(transitive)
+ Addedemoji-regex@8.0.09.2.2(transitive)
+ Addedforeground-child@3.3.0(transitive)
+ Addedglob@10.4.5(transitive)
+ Addedis-fullwidth-code-point@3.0.0(transitive)
+ Addedisexe@2.0.0(transitive)
+ Addedjackspeak@3.4.3(transitive)
+ Addedlru-cache@10.4.3(transitive)
+ Addedminimatch@9.0.5(transitive)
+ Addedminipass@7.1.2(transitive)
+ Addedpackage-json-from-dist@1.0.1(transitive)
+ Addedpath-key@3.1.1(transitive)
+ Addedpath-scurry@1.11.1(transitive)
+ Addedshebang-command@2.0.0(transitive)
+ Addedshebang-regex@3.0.0(transitive)
+ Addedsignal-exit@4.1.0(transitive)
+ Addedstring-width@4.2.35.1.2(transitive)
+ Addedstrip-ansi@6.0.17.1.0(transitive)
+ Addedwhich@2.0.2(transitive)
+ Addedwrap-ansi@7.0.08.1.0(transitive)
- Removed@volvo-cars/market-sites@1.7.3(transitive)
- Removedfs.realpath@1.0.0(transitive)
- Removedglob@8.1.0(transitive)
- Removedinflight@1.0.6(transitive)
- Removedinherits@2.0.4(transitive)
- Removedminimatch@5.1.6(transitive)
- Removedonce@1.4.0(transitive)
- Removedwrappy@1.0.2(transitive)
Updatedglob@^10.3.4