Skip to content
This repository was archived by the owner on Jan 19, 2026. It is now read-only.

Commit 99e259f

Browse files
fix: fetchComponent when shared name prefix or suffix (#198)
* fix: fetchComponent when common name prefix * test: 422 responses when upserting components * fix: finding matching preset when names aren't unique
1 parent 7812ae5 commit 99e259f

4 files changed

Lines changed: 213 additions & 6 deletions

File tree

src/commands/components/pull/actions.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ const mockedComponents = [{
2424
color: null,
2525
internal_tags_list: ['tag'],
2626
internal_tag_ids: [1],
27+
}, {
28+
name: 'name-2',
29+
display_name: 'Name 2',
30+
created_at: '2021-08-09T12:00:00Z',
31+
updated_at: '2021-08-09T12:00:00Z',
32+
id: 12346,
33+
schema: { type: 'object' },
34+
color: null,
35+
internal_tags_list: [],
36+
internal_tag_ids: [],
2737
}];
2838

2939
const handlers = [
@@ -70,6 +80,16 @@ describe('pull components actions', () => {
7080
color: null,
7181
internal_tags_list: ['tag'],
7282
internal_tag_ids: [1],
83+
}, {
84+
name: 'name-2',
85+
display_name: 'Name 2',
86+
created_at: '2021-08-09T12:00:00Z',
87+
updated_at: '2021-08-09T12:00:00Z',
88+
id: 12346,
89+
schema: { type: 'object' },
90+
color: null,
91+
internal_tags_list: [],
92+
internal_tag_ids: [],
7393
}];
7494

7595
const result = await fetchComponents('12345', 'valid-token', 'eu');
@@ -94,6 +114,25 @@ describe('pull components actions', () => {
94114
expect(result).toEqual(mockResponse.components[0]);
95115
});
96116

117+
it('should choose the right component when multiple names match', async () => {
118+
const mockResponse = {
119+
components: [{
120+
name: 'name-2',
121+
display_name: 'Name 2',
122+
created_at: '2021-08-09T12:00:00Z',
123+
updated_at: '2021-08-09T12:00:00Z',
124+
id: 12346,
125+
schema: { type: 'object' },
126+
color: null,
127+
internal_tags_list: [],
128+
internal_tag_ids: [],
129+
}],
130+
};
131+
// searching for 'name-2' would match both 'component-name-2' and 'name-2'
132+
const result = await fetchComponent('12345', 'name-2', 'valid-token', 'eu');
133+
expect(result).toEqual(mockResponse.components[0]);
134+
});
135+
97136
it('should throw an masked error for invalid token', async () => {
98137
await expect(fetchComponents('12345', 'invalid-token', 'eu')).rejects.toThrow(
99138
expect.objectContaining({

src/commands/components/pull/actions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export const fetchComponent = async (space: string, componentName: string, token
3434
Authorization: token,
3535
},
3636
});
37-
return response.components?.[0];
37+
return response.components?.find(c => c.name === componentName);
3838
}
3939
catch (error) {
4040
handleAPIError('pull_components', error as Error, `Failed to fetch component ${componentName}`);

src/commands/components/push/actions.test.ts

Lines changed: 172 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,18 @@ const mockComponent: SpaceComponent = {
1717
internal_tag_ids: ['1'],
1818
};
1919

20+
const mockComponentExisting: SpaceComponent = {
21+
name: 'component-name-2',
22+
display_name: 'Component Name @',
23+
created_at: '2021-08-09T12:00:00Z',
24+
updated_at: '2021-08-09T12:00:00Z',
25+
id: 12346,
26+
schema: { type: 'object' },
27+
color: null,
28+
internal_tags_list: [],
29+
internal_tag_ids: ['1'],
30+
};
31+
2032
const mockComponentGroup: SpaceComponentGroup = {
2133
name: 'group-name',
2234
uuid: 'group-uuid',
@@ -25,6 +37,14 @@ const mockComponentGroup: SpaceComponentGroup = {
2537
parent_uuid: 'parent-uuid',
2638
};
2739

40+
const mockComponentGroupExisting: SpaceComponentGroup = {
41+
name: 'group-name-2',
42+
uuid: 'group-uuid-2',
43+
id: 2,
44+
parent_id: 0,
45+
parent_uuid: 'parent-uuid',
46+
};
47+
2848
const mockComponentPreset: SpaceComponentPreset = {
2949
id: 1,
3050
name: 'preset-name',
@@ -39,17 +59,59 @@ const mockComponentPreset: SpaceComponentPreset = {
3959
description: '',
4060
};
4161

62+
const mockComponentPresetExisting: SpaceComponentPreset = {
63+
id: 2,
64+
name: 'preset-name-2',
65+
component_id: 1,
66+
preset: { field: 'value' },
67+
space_id: 12345,
68+
created_at: '2021-08-09T12:00:00Z',
69+
updated_at: '2021-08-09T12:00:00Z',
70+
image: '',
71+
color: '',
72+
icon: '',
73+
description: '',
74+
};
75+
76+
const mockComponentPresetExistingDifferentComponent: SpaceComponentPreset = {
77+
id: 3,
78+
// component present names aren't globally unique, they're only unique for each component
79+
name: 'preset-name-2',
80+
component_id: 2,
81+
preset: { field: 'value' },
82+
space_id: 12345,
83+
created_at: '2021-08-09T12:00:00Z',
84+
updated_at: '2021-08-09T12:00:00Z',
85+
image: '',
86+
color: '',
87+
icon: '',
88+
description: '',
89+
};
90+
4291
const mockInternalTag: SpaceComponentInternalTag = {
4392
id: 1,
4493
name: 'tag-name',
94+
object_type: 'component',
95+
};
96+
97+
const mockInternalTagExisting: SpaceComponentInternalTag = {
98+
id: 2,
99+
name: 'tag-name-2',
100+
object_type: 'component',
45101
};
46102

47103
const handlers = [
48104
// Component handlers
49105
http.post('https://api.storyblok.com/v1/spaces/12345/components', async ({ request }) => {
50106
const token = request.headers.get('Authorization');
51107
if (token === 'valid-token') {
52-
return HttpResponse.json({ component: mockComponent });
108+
const body: any = await request.json();
109+
if (body.id === mockComponentExisting.id) {
110+
return HttpResponse.json({ name: ['has already been taken'] }, { status: 422 });
111+
}
112+
else {
113+
return HttpResponse.json({ component: mockComponent });
114+
}
53115
}
54116
return new HttpResponse('Unauthorized', { status: 401 });
55117
}),
@@ -60,12 +122,32 @@ const handlers = [
60122
}
61123
return new HttpResponse('Unauthorized', { status: 401 });
62124
}),
125+
http.get('https://api.storyblok.com/v1/spaces/12345/components', async ({ request }) => {
126+
const token = request.headers.get('Authorization');
127+
if (token === 'valid-token') {
128+
return HttpResponse.json({ components: [mockComponentExisting] });
129+
}
130+
return new HttpResponse('Unauthorized', { status: 401 });
131+
}),
132+
http.put('https://api.storyblok.com/v1/spaces/12345/components/12346', async ({ request }) => {
133+
const token = request.headers.get('Authorization');
134+
if (token === 'valid-token') {
135+
return HttpResponse.json({ component: mockComponentExisting });
136+
}
137+
return new HttpResponse('Unauthorized', { status: 401 });
138+
}),
63139

64140
// Component group handlers
65141
http.post('https://api.storyblok.com/v1/spaces/12345/component_groups', async ({ request }) => {
66142
const token = request.headers.get('Authorization');
67143
if (token === 'valid-token') {
68-
return HttpResponse.json({ component_group: mockComponentGroup });
144+
const body: any = await request.json();
145+
if (body.id === mockComponentPresetExisting.id) {
146+
return HttpResponse.json({ name: ['has already been taken'] }, { status: 422 });
147+
}
148+
else {
149+
return HttpResponse.json({ component_group: mockComponentGroup });
150+
}
69151
}
70152
return new HttpResponse('Unauthorized', { status: 401 });
71153
}),
@@ -76,12 +158,32 @@ const handlers = [
76158
}
77159
return new HttpResponse('Unauthorized', { status: 401 });
78160
}),
161+
http.get('https://api.storyblok.com/v1/spaces/12345/component_groups', async ({ request }) => {
162+
const token = request.headers.get('Authorization');
163+
if (token === 'valid-token') {
164+
return HttpResponse.json({ component_groups: [mockComponentGroupExisting] });
165+
}
166+
return new HttpResponse('Unauthorized', { status: 401 });
167+
}),
168+
http.put('https://api.storyblok.com/v1/spaces/12345/component_groups/2', async ({ request }) => {
169+
const token = request.headers.get('Authorization');
170+
if (token === 'valid-token') {
171+
return HttpResponse.json({ component_group: mockComponentGroupExisting });
172+
}
173+
return new HttpResponse('Unauthorized', { status: 401 });
174+
}),
79175

80176
// Component preset handlers
81177
http.post('https://api.storyblok.com/v1/spaces/12345/presets', async ({ request }) => {
82178
const token = request.headers.get('Authorization');
83179
if (token === 'valid-token') {
84-
return HttpResponse.json({ preset: mockComponentPreset });
180+
const body: any = await request.json();
181+
if (body.preset.id === mockComponentPresetExisting.id || body.preset.id === mockComponentPresetExistingDifferentComponent.id) {
182+
return HttpResponse.json({ name: ['has already been taken'] }, { status: 422 });
183+
}
184+
else {
185+
return HttpResponse.json({ preset: mockComponentPreset });
186+
}
85187
}
86188
return new HttpResponse('Unauthorized', { status: 401 });
87189
}),
@@ -92,12 +194,39 @@ const handlers = [
92194
}
93195
return new HttpResponse('Unauthorized', { status: 401 });
94196
}),
197+
http.get('https://api.storyblok.com/v1/spaces/12345/presets', async ({ request }) => {
198+
const token = request.headers.get('Authorization');
199+
if (token === 'valid-token') {
200+
return HttpResponse.json({ presets: [mockComponentPresetExisting, mockComponentPresetExistingDifferentComponent] });
201+
}
202+
return new HttpResponse('Unauthorized', { status: 401 });
203+
}),
204+
http.put('https://api.storyblok.com/v1/spaces/12345/presets/2', async ({ request }) => {
205+
const token = request.headers.get('Authorization');
206+
if (token === 'valid-token') {
207+
return HttpResponse.json({ preset: mockComponentPresetExisting });
208+
}
209+
return new HttpResponse('Unauthorized', { status: 401 });
210+
}),
211+
http.put('https://api.storyblok.com/v1/spaces/12345/presets/3', async ({ request }) => {
212+
const token = request.headers.get('Authorization');
213+
if (token === 'valid-token') {
214+
return HttpResponse.json({ preset: mockComponentPresetExistingDifferentComponent });
215+
}
216+
return new HttpResponse('Unauthorized', { status: 401 });
217+
}),
95218

96219
// Internal tag handlers
97220
http.post('https://api.storyblok.com/v1/spaces/12345/internal_tags', async ({ request }) => {
98221
const token = request.headers.get('Authorization');
99222
if (token === 'valid-token') {
100-
return HttpResponse.json({ internal_tag: mockInternalTag });
223+
const body: any = await request.json();
224+
if (body.id === mockInternalTagExisting.id) {
225+
return HttpResponse.json({ name: ['has already been taken'] }, { status: 422 });
226+
}
227+
else {
228+
return HttpResponse.json({ internal_tag: mockInternalTag });
229+
}
101230
}
102231
return new HttpResponse('Unauthorized', { status: 401 });
103232
}),
@@ -108,6 +237,20 @@ const handlers = [
108237
}
109238
return new HttpResponse('Unauthorized', { status: 401 });
110239
}),
240+
http.get('https://api.storyblok.com/v1/spaces/12345/internal_tags', async ({ request }) => {
241+
const token = request.headers.get('Authorization');
242+
if (token === 'valid-token') {
243+
return HttpResponse.json({ internal_tags: [mockInternalTagExisting] });
244+
}
245+
return new HttpResponse('Unauthorized', { status: 401 });
246+
}),
247+
http.put('https://api.storyblok.com/v1/spaces/12345/internal_tags/2', async ({ request }) => {
248+
const token = request.headers.get('Authorization');
249+
if (token === 'valid-token') {
250+
return HttpResponse.json({ internal_tag: mockInternalTagExisting });
251+
}
252+
return new HttpResponse('Unauthorized', { status: 401 });
253+
}),
111254
];
112255

113256
const server = setupServer(...handlers);
@@ -136,6 +279,11 @@ describe('push components actions', () => {
136279
expect(result).toEqual(mockComponent);
137280
});
138281

282+
it('should upsert existing component successfully with a valid token', async () => {
283+
const result = await upsertComponent('12345', mockComponentExisting, 'valid-token', 'eu');
284+
expect(result).toEqual(mockComponentExisting);
285+
});
286+
139287
it('should throw an error for invalid token', async () => {
140288
await expect(pushComponent('12345', mockComponent, 'invalid-token', 'eu')).rejects.toThrow(
141289
expect.objectContaining({
@@ -179,6 +327,11 @@ describe('push components actions', () => {
179327
const result = await upsertComponentGroup('12345', mockComponentGroup, 'valid-token', 'eu');
180328
expect(result).toEqual(mockComponentGroup);
181329
});
330+
331+
it('should upsert existing component group successfully with a valid token', async () => {
332+
const result = await upsertComponentGroup('12345', mockComponentGroupExisting, 'valid-token', 'eu');
333+
expect(result).toEqual(mockComponentGroupExisting);
334+
});
182335
});
183336

184337
describe('component preset', () => {
@@ -196,6 +349,16 @@ describe('push components actions', () => {
196349
const result = await upsertComponentPreset('12345', mockComponentPreset, 'valid-token', 'eu');
197350
expect(result).toEqual(mockComponentPreset);
198351
});
352+
353+
it('should upsert existing component preset successfully with a valid token', async () => {
354+
const result = await upsertComponentPreset('12345', mockComponentPresetExisting, 'valid-token', 'eu');
355+
expect(result).toEqual(mockComponentPresetExisting);
356+
});
357+
358+
it('should upsert existing component preset with a duplicated name successfully with a valid token', async () => {
359+
const result = await upsertComponentPreset('12345', mockComponentPresetExistingDifferentComponent, 'valid-token', 'eu');
360+
expect(result).toEqual(mockComponentPresetExistingDifferentComponent);
361+
});
199362
});
200363

201364
describe('component internal tag', () => {
@@ -213,6 +376,11 @@ describe('push components actions', () => {
213376
const result = await upsertComponentInternalTag('12345', mockInternalTag, 'valid-token', 'eu');
214377
expect(result).toEqual(mockInternalTag);
215378
});
379+
380+
it('should upsert existing component internal tag successfully with a valid token', async () => {
381+
const result = await upsertComponentInternalTag('12345', mockInternalTagExisting, 'valid-token', 'eu');
382+
expect(result).toEqual(mockInternalTagExisting);
383+
});
216384
});
217385

218386
describe('readComponentsFiles', () => {

src/commands/components/push/actions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ export const upsertComponentPreset = async (space: string, preset: Partial<Space
181181
if (responseData?.name?.[0] === 'has already been taken') {
182182
// Find existing preset by name
183183
const existingPresets = await fetchComponentPresets(space, token, region);
184-
const existingPreset = existingPresets?.find(p => p.name === preset.name);
184+
const existingPreset = existingPresets?.find(p => p.name === preset.name && p.component_id === preset.component_id);
185185
if (existingPreset) {
186186
// Update existing preset
187187
return await updateComponentPreset(space, existingPreset.id, { preset }, token, region);

0 commit comments

Comments
 (0)