diff --git a/README.md b/README.md index 7ebc9127a..fb7b69dd5 100644 --- a/README.md +++ b/README.md @@ -340,12 +340,13 @@ When initialising a client, you will receive an instance of the [`ContentfulClie #### Entries -| Chain | Modifier | -| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| _none (default)_ | Returns entries in a single locale. Resolvable linked entries will be inlined while unresolvable links will be kept as link objects. [Read more on link resolution](ADVANCED.md#link-resolution) | -| `withAllLocales` | Returns entries in all locales. | -| `withoutLinkResolution` | All linked entries will be rendered as link objects. [Read more on link resolution](ADVANCED.md#link-resolution) | -| `withoutUnresolvableLinks` | If linked entries are not resolvable, the corresponding link objects are removed from the response. | +| Chain | Modifier | +| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| _none (default)_ | Returns entries in a single locale. Resolvable linked entries will be inlined while unresolvable links will be kept as link objects. [Read more on link resolution](ADVANCED.md#link-resolution) | +| `withAllLocales` | Returns entries in all locales. | +| `withoutLinkResolution` | All linked entries will be rendered as link objects. [Read more on link resolution](ADVANCED.md#link-resolution) | +| `withoutUnresolvableLinks` | If linked entries are not resolvable, the corresponding link objects are removed from the response. | +| `withLocaleBasedPublishing` | Fetched entries & assets will be returned with only content from published locales. | ##### Example diff --git a/lib/create-contentful-api.ts b/lib/create-contentful-api.ts index b4b2a77cd..3d09fe574 100644 --- a/lib/create-contentful-api.ts +++ b/lib/create-contentful-api.ts @@ -50,6 +50,7 @@ import validateSearchParameters from './utils/validate-search-parameters.js' import { getTimelinePreviewParams } from './utils/timeline-preview-helpers.js' import { normalizeCursorPaginationParameters } from './utils/normalize-cursor-pagination-parameters.js' import { normalizeCursorPaginationResponse } from './utils/normalize-cursor-pagination-response.js' +import type { AxiosRequestConfig } from 'axios' const ASSET_KEY_MAX_LIFETIME = 48 * 60 * 60 @@ -230,6 +231,7 @@ export default function createContentfulApi( withAllLocales: false, withoutLinkResolution: false, withoutUnresolvableLinks: false, + withLocaleBasedPublishing: false, }, ) { const { withAllLocales } = options @@ -280,6 +282,7 @@ export default function createContentfulApi( withAllLocales: false, withoutLinkResolution: false, withoutUnresolvableLinks: false, + withLocaleBasedPublishing: false, }, ) { const { withAllLocales } = options @@ -318,14 +321,22 @@ export default function createContentfulApi( ): Promise< CollectionForQuery, Locales>, Query> > { - const { withoutLinkResolution, withoutUnresolvableLinks } = options + const { withoutLinkResolution, withoutUnresolvableLinks, withLocaleBasedPublishing } = options try { + const baseConfig = createRequestConfig({ + query: prepareQuery(query), + }) + const config: AxiosRequestConfig = baseConfig + if (withLocaleBasedPublishing) { + config.headers = { + ...config.headers, + 'X-Contentful-Locale-Based-Publishing': true, + } + } const entries = await get({ context: 'environment', path: maybeEnableTimelinePreview('entries'), - config: createRequestConfig({ - query: prepareQuery(query), - }), + config, }) return resolveCircular(entries, { @@ -358,6 +369,7 @@ export default function createContentfulApi( withAllLocales: false, withoutLinkResolution: false, withoutUnresolvableLinks: false, + withLocaleBasedPublishing: false, }, ) { const { withAllLocales } = options @@ -367,18 +379,33 @@ export default function createContentfulApi( const localeSpecificQuery = withAllLocales ? { ...query, locale: '*' } : query - return internalGetAssets, Query>(localeSpecificQuery) + return internalGetAssets, Query>( + localeSpecificQuery, + options, + ) } async function internalGetAsset( id: string, query: Record, + options: Options, ): Promise, Locales>> { + const { withLocaleBasedPublishing } = options try { + const baseConfig = createRequestConfig({ + query: prepareQuery(query), + }) + const config: AxiosRequestConfig = baseConfig + if (withLocaleBasedPublishing) { + config.headers = { + ...config.headers, + 'X-Contentful-Locale-Based-Publishing': true, + } + } return get({ context: 'environment', path: maybeEnableTimelinePreview(`assets/${id}`), - config: createRequestConfig({ query: prepareQuery(query) }), + config, }) } catch (error) { errorHandler(error) @@ -392,6 +419,7 @@ export default function createContentfulApi( withAllLocales: false, withoutLinkResolution: false, withoutUnresolvableLinks: false, + withLocaleBasedPublishing: false, }, ) { const { withAllLocales } = options @@ -401,7 +429,11 @@ export default function createContentfulApi( const localeSpecificQuery = withAllLocales ? { ...query, locale: '*' } : query - return internalGetAsset>(id, localeSpecificQuery) + return internalGetAsset>( + id, + localeSpecificQuery, + options, + ) } async function internalGetAssets< @@ -410,14 +442,24 @@ export default function createContentfulApi( Query extends Record, >( query: Query, + options: Options, ): Promise, Locales>, Query>> { + const { withLocaleBasedPublishing } = options try { + const baseConfig = createRequestConfig({ + query: prepareQuery(query), + }) + const config: AxiosRequestConfig = baseConfig + if (withLocaleBasedPublishing) { + config.headers = { + ...config.headers, + 'X-Contentful-Locale-Based-Publishing': true, + } + } return get({ context: 'environment', path: maybeEnableTimelinePreview('assets'), - config: createRequestConfig({ - query: prepareQuery(query), - }), + config, }) } catch (error) { errorHandler(error) @@ -481,6 +523,7 @@ export default function createContentfulApi( withAllLocales: false, withoutLinkResolution: false, withoutUnresolvableLinks: false, + withLocaleBasedPublishing: false, }, ) { validateResolveLinksParam(query) @@ -508,6 +551,7 @@ export default function createContentfulApi( withAllLocales: false, withoutLinkResolution: false, withoutUnresolvableLinks: false, + withLocaleBasedPublishing: false, }, ) { return internalParseEntries>( diff --git a/lib/make-client.ts b/lib/make-client.ts index 8788b7af8..240c02d41 100644 --- a/lib/make-client.ts +++ b/lib/make-client.ts @@ -31,6 +31,9 @@ function create( Object.defineProperty(response, 'withoutUnresolvableLinks', { get: () => makeInnerClient({ ...options, withoutUnresolvableLinks: true }), }) + Object.defineProperty(response, 'withLocaleBasedPublishing', { + get: () => makeInnerClient({ ...options, withLocaleBasedPublishing: true }), + }) return Object.create(response) as ContentfulClientApi> } @@ -50,6 +53,7 @@ export const makeClient = ({ withoutLinkResolution: false, withAllLocales: false, withoutUnresolvableLinks: false, + withLocaleBasedPublishing: false, }, ) @@ -60,6 +64,7 @@ export const makeClient = ({ withAllLocales: true, withoutLinkResolution: false, withoutUnresolvableLinks: false, + withLocaleBasedPublishing: false, }) }, get withoutLinkResolution() { @@ -67,6 +72,7 @@ export const makeClient = ({ withAllLocales: false, withoutLinkResolution: true, withoutUnresolvableLinks: false, + withLocaleBasedPublishing: false, }) }, get withoutUnresolvableLinks() { @@ -74,6 +80,15 @@ export const makeClient = ({ withAllLocales: false, withoutLinkResolution: false, withoutUnresolvableLinks: true, + withLocaleBasedPublishing: false, + }) + }, + get withLocaleBasedPublishing() { + return makeInnerClient>({ + withAllLocales: false, + withoutLinkResolution: false, + withoutUnresolvableLinks: false, + withLocaleBasedPublishing: true, }) }, } diff --git a/lib/types/client.ts b/lib/types/client.ts index 7beb6bd98..8e4935ec1 100644 --- a/lib/types/client.ts +++ b/lib/types/client.ts @@ -36,6 +36,7 @@ export type ChainModifiers = | 'WITH_ALL_LOCALES' | 'WITHOUT_LINK_RESOLUTION' | 'WITHOUT_UNRESOLVABLE_LINKS' + | 'WITH_LOCALE_BASED_PUBLISHING' | undefined /** @@ -581,6 +582,10 @@ export interface ContentfulClientApi { ? never : ContentfulClientApi> + withLocaleBasedPublishing: 'WITH_LOCALE_BASED_PUBLISHING' extends Modifiers + ? never + : ContentfulClientApi> + /** * The current Contentful.js version */ diff --git a/lib/utils/client-helpers.ts b/lib/utils/client-helpers.ts index c18495224..60a8da64f 100644 --- a/lib/utils/client-helpers.ts +++ b/lib/utils/client-helpers.ts @@ -16,6 +16,11 @@ export type ChainOption = { : 'WITHOUT_UNRESOLVABLE_LINKS' extends Modifiers ? true : false + withLocaleBasedPublishing: ChainModifiers extends Modifiers + ? boolean + : 'WITH_LOCALE_BASED_PUBLISHING' extends Modifiers + ? true + : false } export type DefaultChainOption = ChainOption diff --git a/test/types/chain-options.test-d.ts b/test/types/chain-options.test-d.ts index e6f271a19..04e4810a8 100644 --- a/test/types/chain-options.test-d.ts +++ b/test/types/chain-options.test-d.ts @@ -8,40 +8,47 @@ expectAssignable({ withoutLinkResolution: true as boolean, withAllLocales: true as boolean, withoutUnresolvableLinks: true as boolean, + withLocaleBasedPublishing: true as boolean, }) expectType>({ withoutLinkResolution: false, withAllLocales: false, withoutUnresolvableLinks: false, + withLocaleBasedPublishing: false, }) expectType>({ withoutLinkResolution: false, withAllLocales: false, + withLocaleBasedPublishing: false, withoutUnresolvableLinks: true, }) expectType>({ withoutLinkResolution: true, withAllLocales: false, + withLocaleBasedPublishing: false, withoutUnresolvableLinks: false, }) expectType>({ withoutLinkResolution: false, withAllLocales: true, + withLocaleBasedPublishing: false, withoutUnresolvableLinks: false, }) expectType>({ withoutLinkResolution: false, + withLocaleBasedPublishing: false, withAllLocales: true, withoutUnresolvableLinks: true, }) expectNotType>({ withoutLinkResolution: false, + withLocaleBasedPublishing: false, withAllLocales: true, withoutUnresolvableLinks: false, }) @@ -50,4 +57,33 @@ expectType>({ withoutLinkResolution: true, withAllLocales: true, withoutUnresolvableLinks: false, + withLocaleBasedPublishing: false, +}) + +expectType>({ + withoutLinkResolution: false, + withAllLocales: false, + withoutUnresolvableLinks: false, + withLocaleBasedPublishing: true, +}) + +expectType>({ + withoutLinkResolution: false, + withAllLocales: true, + withoutUnresolvableLinks: false, + withLocaleBasedPublishing: true, +}) + +expectType>({ + withoutLinkResolution: true, + withAllLocales: false, + withoutUnresolvableLinks: false, + withLocaleBasedPublishing: true, +}) + +expectType>({ + withoutLinkResolution: false, + withAllLocales: false, + withoutUnresolvableLinks: true, + withLocaleBasedPublishing: true, }) diff --git a/test/types/client/createClient.test-d.ts b/test/types/client/createClient.test-d.ts index 81ba2894f..dbcfb0e55 100644 --- a/test/types/client/createClient.test-d.ts +++ b/test/types/client/createClient.test-d.ts @@ -55,3 +55,29 @@ expectType>( expectType>( createClient(CLIENT_OPTIONS).withAllLocales.withoutUnresolvableLinks, ) +expectType>( + createClient(CLIENT_OPTIONS).withLocaleBasedPublishing, +) +expectType(createClient(CLIENT_OPTIONS).withLocaleBasedPublishing.withLocaleBasedPublishing) + +expectType>( + createClient(CLIENT_OPTIONS).withLocaleBasedPublishing.withAllLocales, +) + +expectType>( + createClient(CLIENT_OPTIONS).withLocaleBasedPublishing.withoutLinkResolution, +) + +expectType>( + createClient(CLIENT_OPTIONS).withLocaleBasedPublishing.withoutUnresolvableLinks, +) + +expectType< + ContentfulClientApi< + 'WITH_LOCALE_BASED_PUBLISHING' | 'WITH_ALL_LOCALES' | 'WITHOUT_LINK_RESOLUTION' + > +>(createClient(CLIENT_OPTIONS).withLocaleBasedPublishing.withAllLocales.withoutLinkResolution) + +expectType>( + createClient(CLIENT_OPTIONS).withAllLocales.withLocaleBasedPublishing, +) diff --git a/test/unit/make-contentful-api-client-chained-modifier.test.ts b/test/unit/make-contentful-api-client-chained-modifier.test.ts index f7be6a09d..530027702 100644 --- a/test/unit/make-contentful-api-client-chained-modifier.test.ts +++ b/test/unit/make-contentful-api-client-chained-modifier.test.ts @@ -203,4 +203,412 @@ describe('Contentful API client chain modifiers', () => { }) }) }) + + describe('withLocaleBasedPublishing', () => { + beforeEach(() => { + vi.clearAllMocks() + resolveCircularMock.default.mockImplementation((args) => { + return args + }) + }) + + describe('getEntries', () => { + it('adds X-Contentful-Locale-Based-Publishing header when calling getEntries', async () => { + const { api, getStub } = setupWithData({ + promise: Promise.resolve({ data }), + }) + + await api.withLocaleBasedPublishing.getEntries() + + expect(getStub).toHaveBeenCalledWith( + expect.stringContaining('entries'), + expect.objectContaining({ + headers: { + 'X-Contentful-Locale-Based-Publishing': true, + }, + }), + ) + }) + + it('adds header when chained with withAllLocales', async () => { + const { api, getStub } = setupWithData({ + promise: Promise.resolve({ data }), + }) + + await api.withLocaleBasedPublishing.withAllLocales.getEntries() + + expect(getStub).toHaveBeenCalledWith( + expect.stringContaining('entries'), + expect.objectContaining({ + headers: { + 'X-Contentful-Locale-Based-Publishing': true, + }, + }), + ) + }) + + it('adds header when chained with withoutLinkResolution', async () => { + const { api, getStub } = setupWithData({ + promise: Promise.resolve({ data }), + }) + + await api.withLocaleBasedPublishing.withoutLinkResolution.getEntries() + + expect(getStub).toHaveBeenCalledWith( + expect.stringContaining('entries'), + expect.objectContaining({ + headers: { + 'X-Contentful-Locale-Based-Publishing': true, + }, + }), + ) + }) + + it('adds header when chained with withoutUnresolvableLinks', async () => { + const { api, getStub } = setupWithData({ + promise: Promise.resolve({ data }), + }) + + await api.withLocaleBasedPublishing.withoutUnresolvableLinks.getEntries() + + expect(getStub).toHaveBeenCalledWith( + expect.stringContaining('entries'), + expect.objectContaining({ + headers: { + 'X-Contentful-Locale-Based-Publishing': true, + }, + }), + ) + }) + + it('adds header when chained with multiple modifiers', async () => { + const { api, getStub } = setupWithData({ + promise: Promise.resolve({ data }), + }) + + await api.withLocaleBasedPublishing.withAllLocales.withoutLinkResolution.getEntries() + + expect(getStub).toHaveBeenCalledWith( + expect.stringContaining('entries'), + expect.objectContaining({ + headers: { + 'X-Contentful-Locale-Based-Publishing': true, + }, + }), + ) + }) + + it('does not add header when not using withLocaleBasedPublishing', async () => { + const { api, getStub } = setupWithData({ + promise: Promise.resolve({ data }), + }) + + await api.getEntries() + + const callArgs = getStub.mock.calls[0] + const config = callArgs[1] + expect(config?.headers).toBeUndefined() + }) + }) + + describe('getEntry', () => { + it('adds X-Contentful-Locale-Based-Publishing header when calling getEntry', async () => { + const { api, getStub } = setupWithData({ + promise: Promise.resolve({ data }), + }) + + await api.withLocaleBasedPublishing.getEntry('test-id') + + expect(getStub).toHaveBeenCalledWith( + expect.stringContaining('entries'), + expect.objectContaining({ + headers: { + 'X-Contentful-Locale-Based-Publishing': true, + }, + }), + ) + }) + + it('adds header when chained with withAllLocales for getEntry', async () => { + const { api, getStub } = setupWithData({ + promise: Promise.resolve({ data }), + }) + + await api.withLocaleBasedPublishing.withAllLocales.getEntry('test-id') + + expect(getStub).toHaveBeenCalledWith( + expect.stringContaining('entries'), + expect.objectContaining({ + headers: { + 'X-Contentful-Locale-Based-Publishing': true, + }, + }), + ) + }) + + it('does not add header when not using withLocaleBasedPublishing', async () => { + const { api, getStub } = setupWithData({ + promise: Promise.resolve({ data }), + }) + + await api.getEntry('test-id') + + const callArgs = getStub.mock.calls[0] + const config = callArgs[1] + expect(config?.headers).toBeUndefined() + }) + }) + + describe('getAssets', () => { + it('adds X-Contentful-Locale-Based-Publishing header when calling getAssets', async () => { + const assetData = { + items: [ + { + sys: { id: 'asset-id', type: 'Asset' }, + fields: { title: 'Test Asset' }, + }, + ], + } + const { api, getStub } = setupWithData({ + promise: Promise.resolve({ data: assetData }), + }) + + await api.withLocaleBasedPublishing.getAssets() + + expect(getStub).toHaveBeenCalledWith( + expect.stringContaining('assets'), + expect.objectContaining({ + headers: { + 'X-Contentful-Locale-Based-Publishing': true, + }, + }), + ) + }) + + it('adds header when chained with withAllLocales for getAssets', async () => { + const assetData = { + items: [ + { + sys: { id: 'asset-id', type: 'Asset' }, + fields: { title: 'Test Asset' }, + }, + ], + } + const { api, getStub } = setupWithData({ + promise: Promise.resolve({ data: assetData }), + }) + + await api.withLocaleBasedPublishing.withAllLocales.getAssets() + + expect(getStub).toHaveBeenCalledWith( + expect.stringContaining('assets'), + expect.objectContaining({ + headers: { + 'X-Contentful-Locale-Based-Publishing': true, + }, + }), + ) + }) + + it('does not add header when not using withLocaleBasedPublishing', async () => { + const assetData = { + items: [ + { + sys: { id: 'asset-id', type: 'Asset' }, + fields: { title: 'Test Asset' }, + }, + ], + } + const { api, getStub } = setupWithData({ + promise: Promise.resolve({ data: assetData }), + }) + + await api.getAssets() + + const callArgs = getStub.mock.calls[0] + const config = callArgs[1] + expect(config?.headers).toBeUndefined() + }) + }) + + describe('getAsset', () => { + it('adds X-Contentful-Locale-Based-Publishing header when calling getAsset', async () => { + const assetData = { + sys: { id: 'asset-id', type: 'Asset' }, + fields: { title: 'Test Asset' }, + } + const { api, getStub } = setupWithData({ + promise: Promise.resolve({ data: assetData }), + }) + + await api.withLocaleBasedPublishing.getAsset('test-asset-id') + + expect(getStub).toHaveBeenCalledWith( + expect.stringContaining('assets/test-asset-id'), + expect.objectContaining({ + headers: { + 'X-Contentful-Locale-Based-Publishing': true, + }, + }), + ) + }) + + it('adds header when chained with withAllLocales for getAsset', async () => { + const assetData = { + sys: { id: 'asset-id', type: 'Asset' }, + fields: { title: 'Test Asset' }, + } + const { api, getStub } = setupWithData({ + promise: Promise.resolve({ data: assetData }), + }) + + await api.withLocaleBasedPublishing.withAllLocales.getAsset('test-asset-id') + + expect(getStub).toHaveBeenCalledWith( + expect.stringContaining('assets/test-asset-id'), + expect.objectContaining({ + headers: { + 'X-Contentful-Locale-Based-Publishing': true, + }, + }), + ) + }) + + it('does not add header when not using withLocaleBasedPublishing', async () => { + const assetData = { + sys: { id: 'asset-id', type: 'Asset' }, + fields: { title: 'Test Asset' }, + } + const { api, getStub } = setupWithData({ + promise: Promise.resolve({ data: assetData }), + }) + + await api.getAsset('test-asset-id') + + const callArgs = getStub.mock.calls[0] + const config = callArgs[1] + expect(config?.headers).toBeUndefined() + }) + }) + + describe('getEntriesWithCursor', () => { + it('adds X-Contentful-Locale-Based-Publishing header when calling getEntriesWithCursor', async () => { + const cursorData = { + items: [ + { + sys: { id: 'entry-id' }, + }, + ], + sys: { + type: 'Array', + }, + pages: { + next: null, + prev: null, + }, + } + const { api, getStub } = setupWithData({ + promise: Promise.resolve({ data: cursorData }), + }) + + await api.withLocaleBasedPublishing.getEntriesWithCursor() + + expect(getStub).toHaveBeenCalledWith( + expect.stringContaining('entries'), + expect.objectContaining({ + headers: { + 'X-Contentful-Locale-Based-Publishing': true, + }, + }), + ) + }) + }) + + describe('getAssetsWithCursor', () => { + it('adds X-Contentful-Locale-Based-Publishing header when calling getAssetsWithCursor', async () => { + const cursorData = { + items: [ + { + sys: { id: 'asset-id', type: 'Asset' }, + fields: { title: 'Test Asset' }, + }, + ], + sys: { + type: 'Array', + }, + pages: { + next: null, + prev: null, + }, + } + const { api, getStub } = setupWithData({ + promise: Promise.resolve({ data: cursorData }), + }) + + await api.withLocaleBasedPublishing.getAssetsWithCursor() + + expect(getStub).toHaveBeenCalledWith( + expect.stringContaining('assets'), + expect.objectContaining({ + headers: { + 'X-Contentful-Locale-Based-Publishing': true, + }, + }), + ) + }) + }) + + describe('chaining behavior', () => { + it('allows chaining in any order', async () => { + const { api, getStub } = setupWithData({ + promise: Promise.resolve({ data }), + }) + + await api.withAllLocales.withLocaleBasedPublishing.getEntries() + + expect(getStub).toHaveBeenCalledWith( + expect.stringContaining('entries'), + expect.objectContaining({ + headers: { + 'X-Contentful-Locale-Based-Publishing': true, + }, + }), + ) + }) + + it('allows chaining with withoutLinkResolution first', async () => { + const { api, getStub } = setupWithData({ + promise: Promise.resolve({ data }), + }) + + await api.withoutLinkResolution.withLocaleBasedPublishing.getEntries() + + expect(getStub).toHaveBeenCalledWith( + expect.stringContaining('entries'), + expect.objectContaining({ + headers: { + 'X-Contentful-Locale-Based-Publishing': true, + }, + }), + ) + }) + + it('allows chaining with withoutUnresolvableLinks first', async () => { + const { api, getStub } = setupWithData({ + promise: Promise.resolve({ data }), + }) + + await api.withoutUnresolvableLinks.withLocaleBasedPublishing.getEntries() + + expect(getStub).toHaveBeenCalledWith( + expect.stringContaining('entries'), + expect.objectContaining({ + headers: { + 'X-Contentful-Locale-Based-Publishing': true, + }, + }), + ) + }) + }) + }) })