Skip to content

Commit

Permalink
feat: extend SDK with content source maps support [TOL-2043] (#2209)
Browse files Browse the repository at this point in the history
  • Loading branch information
2wce committed Apr 29, 2024
1 parent 8306624 commit eb2cebd
Show file tree
Hide file tree
Showing 12 changed files with 455 additions and 31 deletions.
16 changes: 14 additions & 2 deletions lib/contentful.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
* with access to the Contentful Content Delivery API.
*/

import type { AxiosAdapter, AxiosRequestConfig, AxiosResponse } from 'axios'
import axios from 'axios'
import { createHttpClient, getUserAgentHeader } from 'contentful-sdk-core'
import { createGlobalOptions } from './create-global-options'
import { makeClient } from './make-client'
import type { AxiosAdapter, AxiosRequestConfig, AxiosResponse } from 'axios'
import { validateRemoveUnresolvedParam, validateResolveLinksParam } from './utils/validate-params'
import { ContentfulClientApi } from './types'
import { validateRemoveUnresolvedParam, validateResolveLinksParam } from './utils/validate-params'

/**
* @category Client
Expand Down Expand Up @@ -109,6 +109,18 @@ export interface CreateClientParams {
* Interceptor called on every response. Takes Axios response object as an arg.
*/
responseLogger?: (response: AxiosResponse<any> | Error) => unknown

/**
* Enable alpha features.
*/
alphaFeatures?: {
/**
* Enable Content Source Maps.
* @remarks
* This feature is only available when using the Content Preview API.
*/
withContentSourceMaps?: boolean
}
}

/**
Expand Down
42 changes: 31 additions & 11 deletions lib/create-contentful-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import { AxiosInstance, createRequestConfig, errorHandler } from 'contentful-sdk-core'
import { CreateClientParams } from './contentful'
import { GetGlobalOptions } from './create-global-options'
import pagedSync from './paged-sync'
import {
Expand All @@ -13,27 +14,28 @@ import {
ContentfulClientApi,
ContentType,
ContentTypeCollection,
LocaleCollection,
EntryCollection,
EntrySkeletonType,
LocaleCode,
LocaleCollection,
Space,
SyncOptions,
SyncQuery,
Tag,
TagCollection,
EntryCollection,
SyncQuery,
SyncOptions,
EntrySkeletonType,
} from './types'
import { ChainOptions, ModifiersFromOptions } from './utils/client-helpers'
import normalizeSearchParameters from './utils/normalize-search-parameters'
import normalizeSelect from './utils/normalize-select'
import resolveCircular from './utils/resolve-circular'
import validateTimestamp from './utils/validate-timestamp'
import { ChainOptions, ModifiersFromOptions } from './utils/client-helpers'
import {
checkIncludeContentSourceMapsParamIsAllowed,
validateLocaleParam,
validateRemoveUnresolvedParam,
validateResolveLinksParam,
} from './utils/validate-params'
import validateSearchParameters from './utils/validate-search-parameters'
import validateTimestamp from './utils/validate-timestamp'

const ASSET_KEY_MAX_LIFETIME = 48 * 60 * 60

Expand Down Expand Up @@ -96,6 +98,20 @@ export default function createContentfulApi<OptionType extends ChainOptions>(
return baseUrl
}

function maybeEnableSourceMaps(query: Record<string, any> = {}): Record<string, any> {
const alphaFeatures = (http.httpClientParams as any as CreateClientParams)?.alphaFeatures

const host = http.httpClientParams?.host

const areAllowed = checkIncludeContentSourceMapsParamIsAllowed(host, alphaFeatures)

if (areAllowed) {
query.includeContentSourceMaps = true
}

return query
}

async function get<T>({ context, path, config }: GetConfig): Promise<T> {
const baseUrl = getBaseUrl(context)

Expand Down Expand Up @@ -177,7 +193,7 @@ export default function createContentfulApi<OptionType extends ChainOptions>(
}
try {
const response = await internalGetEntries<EntrySkeletonType<EntrySkeleton>, Locales, Options>(
{ 'sys.id': id, ...query },
{ 'sys.id': id, ...maybeEnableSourceMaps(query) },
options,
)
if (response.items.length > 0) {
Expand Down Expand Up @@ -230,7 +246,9 @@ export default function createContentfulApi<OptionType extends ChainOptions>(
const entries = await get({
context: 'environment',
path: 'entries',
config: createRequestConfig({ query: normalizeSearchParameters(normalizeSelect(query)) }),
config: createRequestConfig({
query: maybeEnableSourceMaps(normalizeSearchParameters(normalizeSelect(query))),
}),
})

return resolveCircular(entries, {
Expand Down Expand Up @@ -276,7 +294,7 @@ export default function createContentfulApi<OptionType extends ChainOptions>(
return get({
context: 'environment',
path: `assets/${id}`,
config: createRequestConfig({ query: normalizeSelect(query) }),
config: createRequestConfig({ query: maybeEnableSourceMaps(normalizeSelect(query)) }),
})
} catch (error) {
errorHandler(error)
Expand Down Expand Up @@ -309,7 +327,9 @@ export default function createContentfulApi<OptionType extends ChainOptions>(
return get({
context: 'environment',
path: 'assets',
config: createRequestConfig({ query: normalizeSearchParameters(normalizeSelect(query)) }),
config: createRequestConfig({
query: maybeEnableSourceMaps(normalizeSearchParameters(normalizeSelect(query))),
}),
})
} catch (error) {
errorHandler(error)
Expand Down
4 changes: 4 additions & 0 deletions lib/types/collection.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { AssetSys } from './asset'
import { EntrySys } from './entry'

/**
* A wrapper object containing additional information for
* a collection of Contentful resources
Expand All @@ -9,4 +12,5 @@ export interface ContentfulCollection<T> {
skip: number
limit: number
items: Array<T>
sys?: AssetSys | EntrySys
}
27 changes: 27 additions & 0 deletions lib/types/sys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,31 @@ export interface EntitySys extends BaseSys {
space: { sys: SpaceLink }
environment: { sys: EnvironmentLink }
locale?: string
contentSourceMaps?: ContentSourceMaps
contentSourceMapsLookup?: ContentSourceMapsLookup
}

export type ContentSourceMaps = {
sys: {
type: 'ContentSourceMaps'
}
mappings: Record<
string,
{
source: {
fieldType: number
editorInterface: number
}
}
>
}

export type ContentSourceMapsLookup = {
sys: {
type: 'ContentSourceMapsLookup'
}
fieldType: string[]
editorInterface: {
[key: string]: string
}[]
}
44 changes: 42 additions & 2 deletions lib/utils/validate-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ function checkLocaleParamIsAll(query) {
if (query.locale === '*') {
throw new ValidationError(
'locale',
`The use of locale='*' is no longer supported.To fetch an entry in all existing locales,
`The use of locale='*' is no longer supported.To fetch an entry in all existing locales,
use client.withAllLocales instead of the locale='*' parameter.`,
)
}
Expand All @@ -29,7 +29,7 @@ export function validateResolveLinksParam(query) {
if ('resolveLinks' in query) {
throw new ValidationError(
'resolveLinks',
`The use of the 'resolveLinks' parameter is no longer supported. By default, links are resolved.
`The use of the 'resolveLinks' parameter is no longer supported. By default, links are resolved.
If you do not want to resolve links, use client.withoutLinkResolution.`,
)
}
Expand All @@ -46,3 +46,43 @@ export function validateRemoveUnresolvedParam(query) {
}
return
}

export function checkIncludeContentSourceMapsParamIsValid(alphaFeatures?: Record<string, any>) {
if (!alphaFeatures) {
return false
}

const isValidWithContentSourceMaps =
'withContentSourceMaps' in alphaFeatures &&
typeof alphaFeatures.withContentSourceMaps === 'boolean'

if (!isValidWithContentSourceMaps) {
throw new ValidationError(
'withContentSourceMaps',
`The 'withContentSourceMaps' parameter must be a boolean.`,
)
}

return true
}

export function checkIncludeContentSourceMapsParamIsAllowed(
host?: string,
alphaFeatures?: Record<string, any>,
) {
if (!alphaFeatures || Object.keys(alphaFeatures).length === 0) {
return false
}

const withContentSourceMapsIsAllowed = host === 'preview.contentful.com'

if (checkIncludeContentSourceMapsParamIsValid(alphaFeatures) && !withContentSourceMapsIsAllowed) {
throw new ValidationError(
'withContentSourceMaps',
`The 'withContentSourceMaps' parameter can only be used with the CPA. Please set host to 'preview.contentful.com' to include Content Source Maps.
`,
)
}

return alphaFeatures.withContentSourceMaps as boolean
}
35 changes: 34 additions & 1 deletion test/integration/getAsset.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import * as contentful from '../../lib/contentful'
import { params } from './utils'
import { ValidationError } from '../../lib/utils/validation-error'
import { params, previewParamsWithCSM } from './utils'

if (process.env.API_INTEGRATION_TESTS) {
params.host = '127.0.0.1:5000'
params.insecure = true
}

const client = contentful.createClient(params)
const invalidClient = contentful.createClient({
...params,
alphaFeatures: { withContentSourceMaps: true },
})
const previewClient = contentful.createClient(previewParamsWithCSM)

describe('getAsset', () => {
const asset = '1x0xpXu4pSGS4OukSyWGUK'
Expand All @@ -24,4 +30,31 @@ describe('getAsset', () => {
expect(response.fields).toBeDefined()
expect(typeof response.fields.title).toBe('object')
})

describe('has (alpha) withContentSourceMaps enabled', () => {
test('cdn client', async () => {
await expect(invalidClient.getAsset(asset)).rejects.toThrow(
`The 'withContentSourceMaps' parameter can only be used with the CPA. Please set host to 'preview.contentful.com' to include Content Source Maps.`,
)
await expect(invalidClient.getAsset(asset)).rejects.toThrow(ValidationError)
})

test('preview client', async () => {
const response = await previewClient.getAsset(asset)

expect(response.fields).toBeDefined()
expect(typeof response.fields.title).toBe('string')
expect(response.sys.contentSourceMaps).toBeDefined()
expect(response.sys?.contentSourceMapsLookup).toBeDefined()
})

test('preview client withAllLocales modifier', async () => {
const response = await previewClient.withAllLocales.getAsset(asset)

expect(response.fields).toBeDefined()
expect(typeof response.fields.title).toBe('object')
expect(response.sys.contentSourceMaps).toBeDefined()
expect(response.sys?.contentSourceMapsLookup).toBeDefined()
})
})
})
45 changes: 44 additions & 1 deletion test/integration/getAssets.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import * as contentful from '../../lib/contentful'
import { params } from './utils'
import { ValidationError } from '../../lib/utils/validation-error'
import { params, previewParamsWithCSM } from './utils'

if (process.env.API_INTEGRATION_TESTS) {
params.host = '127.0.0.1:5000'
params.insecure = true
}

const client = contentful.createClient(params)
const invalidClient = contentful.createClient({
...params,
alphaFeatures: { withContentSourceMaps: true },
})
const previewClient = contentful.createClient(previewParamsWithCSM)

describe('getAssets', () => {
test('default client', async () => {
Expand All @@ -32,4 +38,41 @@ describe('getAssets', () => {
expect(typeof item.fields.title).toBe('object')
})
})

describe('has (alpha) withContentSourceMaps enabled', () => {
test('cdn client', async () => {
await expect(invalidClient.getAssets()).rejects.toThrow(
`The 'withContentSourceMaps' parameter can only be used with the CPA. Please set host to 'preview.contentful.com' to include Content Source Maps.`,
)
await expect(invalidClient.getAssets()).rejects.toThrow(ValidationError)
})

test('preview client', async () => {
const response = await previewClient.getAssets()

expect(response.items).not.toHaveLength(0)

response.items.forEach((item) => {
expect(item.sys.type).toEqual('Asset')
expect(item.fields).toBeDefined()
expect(typeof item.fields.title).toBe('string')
})

expect(response.sys?.contentSourceMapsLookup).toBeDefined()
})

test('preview client withAllLocales modifier', async () => {
const response = await previewClient.withAllLocales.getAssets()

expect(response.items).not.toHaveLength(0)

response.items.forEach((item) => {
expect(item.sys.type).toEqual('Asset')
expect(item.fields).toBeDefined()
expect(typeof item.fields.title).toBe('object')
})

expect(response.sys?.contentSourceMapsLookup).toBeDefined()
})
})
})
Loading

0 comments on commit eb2cebd

Please sign in to comment.