Skip to content

Commit eabb521

Browse files
authored
feat: support templateId in CreateExperienceProps for template-backed experiences [DX-982] (#3006)
* feat: support templateId in CreateExperienceProps discriminated union [DX-982] * fix: pass templateId in update adapter for template-backed experiences [DX-982] * test: add templateId create/update tests and update plain client JSDoc [DX-982] * chore: prettier
1 parent 62685cb commit eabb521

4 files changed

Lines changed: 128 additions & 5 deletions

File tree

lib/adapters/REST/endpoints/experience.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,14 @@ export const update: RestEndpoint<'Experience', 'update'> = (
5757
rawData: UpdateExperienceProps,
5858
headers?: RawAxiosRequestHeaders,
5959
) => {
60-
const data: SetOptional<typeof rawData, 'sys'> & { componentTypeId?: string } = copy(rawData)
60+
const data: SetOptional<typeof rawData, 'sys'> & {
61+
componentTypeId?: string
62+
templateId?: string
63+
} = copy(rawData)
6164
if (rawData.sys.componentType) {
6265
data.componentTypeId = rawData.sys.componentType.sys.id
66+
} else if (rawData.sys.template) {
67+
data.templateId = rawData.sys.template.sys.id
6368
}
6469
delete data.sys
6570
return raw.put<ExperienceProps>(http, getBaseUrl(params) + `/${params.experienceId}`, data, {

lib/entities/experience.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,13 @@ export type ExperienceQueryOptions = CursorPaginationParams & {
7272
// Omit the payload entirely for a full publish (all locales).
7373
export type ExperienceLocalePublishPayload = { add: string[] } | { remove: string[] }
7474

75-
// Create payload — no sys, uses componentTypeId instead of sys.componentType link
76-
export type CreateExperienceProps = ExperienceCommonProps & {
77-
componentTypeId: string
78-
}
75+
// Create payload — no sys, uses either componentTypeId (component-type-backed) or
76+
// templateId (template-backed). The two fields are mutually exclusive.
77+
export type CreateExperienceProps = ExperienceCommonProps &
78+
(
79+
| { componentTypeId: string; templateId?: never }
80+
| { templateId: string; componentTypeId?: never }
81+
)
7982

8083
export type UpdateExperienceProps = ExperienceProps
8184

lib/plain/entities/experience.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export type ExperiencePlainClientAPI = {
6060
* @internal - Experimental endpoint, subject to breaking changes without notice
6161
* @example
6262
* ```javascript
63+
* // Component-type-backed experience
6364
* const experience = await client.experience.create({
6465
* spaceId: '<space_id>',
6566
* environmentId: '<environment_id>',
@@ -72,6 +73,20 @@ export type ExperiencePlainClientAPI = {
7273
* designProperties: {},
7374
* dimensionKeyMap: { designProperties: {} },
7475
* });
76+
*
77+
* // Template-backed experience
78+
* const experience = await client.experience.create({
79+
* spaceId: '<space_id>',
80+
* environmentId: '<environment_id>',
81+
* }, {
82+
* name: 'My Template Experience',
83+
* description: 'A template-backed experience',
84+
* templateId: '<template_id>',
85+
* viewports: [],
86+
* contentProperties: {},
87+
* designProperties: {},
88+
* dimensionKeyMap: { designProperties: {} },
89+
* });
7590
* ```
7691
*/
7792
create(

test/unit/adapters/REST/endpoints/experience.test.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,55 @@ describe('Rest Experience', { concurrent: true }, () => {
251251
})
252252
})
253253

254+
test('create sends templateId when creating a template-backed experience', async () => {
255+
const mockResponse = {
256+
sys: { id: 'new-experience-456', type: 'Experience', version: 1 },
257+
name: 'Template Experience',
258+
description: 'A template-backed experience',
259+
viewports: [],
260+
contentProperties: {},
261+
designProperties: {},
262+
dimensionKeyMap: { designProperties: {} },
263+
}
264+
265+
const { httpMock, adapterMock } = setupRestAdapter(Promise.resolve({ data: mockResponse }))
266+
267+
return adapterMock
268+
.makeRequest({
269+
entityType: 'Experience',
270+
action: 'create',
271+
userAgent: 'mocked',
272+
params: {
273+
spaceId: 'space123',
274+
environmentId: 'master',
275+
},
276+
payload: {
277+
name: 'Template Experience',
278+
description: 'A template-backed experience',
279+
templateId: 'tmpl-789',
280+
viewports: [],
281+
contentProperties: {},
282+
designProperties: {},
283+
dimensionKeyMap: { designProperties: {} },
284+
},
285+
})
286+
.then((r) => {
287+
expect(r).to.eql(mockResponse)
288+
expect(httpMock.post.mock.calls[0][0]).to.eql(
289+
'/spaces/space123/environments/master/experiences',
290+
)
291+
expect(httpMock.post.mock.calls[0][1]).to.eql({
292+
name: 'Template Experience',
293+
description: 'A template-backed experience',
294+
templateId: 'tmpl-789',
295+
viewports: [],
296+
contentProperties: {},
297+
designProperties: {},
298+
dimensionKeyMap: { designProperties: {} },
299+
})
300+
})
301+
})
302+
254303
test('update calls correct URL with version header', async () => {
255304
const mockResponse = {
256305
sys: { id: 'experience123', type: 'Experience', version: 2 },
@@ -301,6 +350,57 @@ describe('Rest Experience', { concurrent: true }, () => {
301350
})
302351
})
303352

353+
test('update sends templateId when experience is template-backed', async () => {
354+
const mockResponse = {
355+
sys: { id: 'experience123', type: 'Experience', version: 2 },
356+
name: 'Updated Experience',
357+
description: 'A template-backed experience',
358+
viewports: [],
359+
contentProperties: {},
360+
designProperties: {},
361+
dimensionKeyMap: { designProperties: {} },
362+
}
363+
364+
const { httpMock, adapterMock } = setupRestAdapter(Promise.resolve({ data: mockResponse }))
365+
366+
return adapterMock
367+
.makeRequest({
368+
entityType: 'Experience',
369+
action: 'update',
370+
userAgent: 'mocked',
371+
params: {
372+
spaceId: 'space123',
373+
environmentId: 'master',
374+
experienceId: 'experience123',
375+
},
376+
payload: {
377+
sys: {
378+
version: 1,
379+
template: {
380+
sys: { type: 'Link', linkType: 'Template', id: 'tmpl-456' },
381+
},
382+
},
383+
name: 'Updated Experience',
384+
description: 'A template-backed experience',
385+
viewports: [],
386+
contentProperties: {},
387+
designProperties: {},
388+
dimensionKeyMap: { designProperties: {} },
389+
},
390+
})
391+
.then((r) => {
392+
expect(r).to.eql(mockResponse)
393+
expect(httpMock.put.mock.calls[0][0]).to.eql(
394+
'/spaces/space123/environments/master/experiences/experience123',
395+
)
396+
expect(httpMock.put.mock.calls[0][2].headers['X-Contentful-Version']).to.eql(1)
397+
const body = httpMock.put.mock.calls[0][1]
398+
expect(body.templateId).to.eql('tmpl-456')
399+
expect(body.componentTypeId).to.be.undefined
400+
expect(body.sys).to.be.undefined
401+
})
402+
})
403+
304404
test('delete calls correct URL', async () => {
305405
const { httpMock, adapterMock } = setupRestAdapter(Promise.resolve({ data: {} }))
306406

0 commit comments

Comments
 (0)