@storyblok/management-api-client 0.2.3 → 1.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.cjs +190 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +624 -0
- package/dist/client.d.mts +624 -0
- package/dist/client.mjs +189 -0
- package/dist/client.mjs.map +1 -0
- package/dist/error.cjs.map +1 -1
- package/dist/error.d.cts +12 -2
- package/dist/error.d.mts +12 -2
- package/dist/error.mjs.map +1 -1
- package/dist/generated/asset_folders/types.gen.d.cts +5 -31
- package/dist/generated/asset_folders/types.gen.d.mts +5 -31
- package/dist/generated/assets/types.gen.d.cts +1 -73
- package/dist/generated/assets/types.gen.d.mts +1 -73
- package/dist/generated/component_folders/types.gen.d.cts +29 -45
- package/dist/generated/component_folders/types.gen.d.mts +29 -45
- package/dist/generated/components/types.gen.d.cts +636 -465
- package/dist/generated/components/types.gen.d.mts +636 -465
- package/dist/generated/datasource_entries/types.gen.d.cts +6 -32
- package/dist/generated/datasource_entries/types.gen.d.mts +6 -32
- package/dist/generated/datasources/types.gen.d.cts +18 -39
- package/dist/generated/datasources/types.gen.d.mts +18 -39
- package/dist/generated/internal_tags/types.gen.d.cts +28 -32
- package/dist/generated/internal_tags/types.gen.d.mts +28 -32
- package/dist/generated/presets/types.gen.d.cts +53 -58
- package/dist/generated/presets/types.gen.d.mts +53 -58
- package/dist/generated/spaces/types.gen.d.cts +1 -41
- package/dist/generated/spaces/types.gen.d.mts +1 -41
- package/dist/generated/stories/types.gen.d.cts +206 -198
- package/dist/generated/stories/types.gen.d.mts +206 -198
- package/dist/generated/users/types.gen.d.cts +13 -112
- package/dist/generated/users/types.gen.d.mts +13 -112
- package/dist/index.cjs +3 -178
- package/dist/index.d.cts +8 -425
- package/dist/index.d.mts +8 -425
- package/dist/index.mjs +2 -176
- package/dist/resources/asset-folders.cjs.map +1 -1
- package/dist/resources/asset-folders.mjs.map +1 -1
- package/dist/resources/assets.cjs +11 -2
- package/dist/resources/assets.cjs.map +1 -1
- package/dist/resources/assets.d.cts +3 -2
- package/dist/resources/assets.d.mts +3 -2
- package/dist/resources/assets.mjs +11 -2
- package/dist/resources/assets.mjs.map +1 -1
- package/dist/resources/component-folders.cjs.map +1 -1
- package/dist/resources/component-folders.mjs.map +1 -1
- package/dist/resources/components.cjs.map +1 -1
- package/dist/resources/components.mjs.map +1 -1
- package/dist/resources/datasource-entries.cjs.map +1 -1
- package/dist/resources/datasource-entries.mjs.map +1 -1
- package/dist/resources/datasources.cjs.map +1 -1
- package/dist/resources/datasources.mjs.map +1 -1
- package/dist/resources/internal-tags.cjs.map +1 -1
- package/dist/resources/internal-tags.mjs.map +1 -1
- package/dist/resources/presets.cjs.map +1 -1
- package/dist/resources/presets.mjs.map +1 -1
- package/dist/resources/spaces.cjs.map +1 -1
- package/dist/resources/spaces.mjs.map +1 -1
- package/dist/resources/stories.cjs.map +1 -1
- package/dist/resources/stories.d.cts +105 -0
- package/dist/resources/stories.d.mts +105 -0
- package/dist/resources/stories.mjs.map +1 -1
- package/dist/resources/users.cjs.map +1 -1
- package/dist/resources/users.mjs.map +1 -1
- package/package.json +13 -10
- package/test/GUIDE.md +59 -0
- package/test/setup.e2e.ts +11 -0
- package/test/specs/mapi-round-trip.spec.e2e.ts +520 -0
- package/vitest.config.e2e.ts +26 -0
- package/vitest.config.ts +3 -0
- package/dist/index.cjs.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/dist/types.d.cts +0 -130
- package/dist/types.d.mts +0 -130
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end test: @storyblok/schema define functions → MAPI round-trip
|
|
3
|
+
*
|
|
4
|
+
* Validates that:
|
|
5
|
+
* 1. Every major MAPI define function produces a valid creation payload.
|
|
6
|
+
* 2. The MAPI accepts those payloads and creates real resources.
|
|
7
|
+
* 3. The types returned by the schema-aware mapi-client match the actual
|
|
8
|
+
* runtime data returned by the API (no type lies).
|
|
9
|
+
* 4. Zod schemas generated from the OpenAPI spec accept real API responses.
|
|
10
|
+
*
|
|
11
|
+
* Run manually (never in CI):
|
|
12
|
+
* pnpm --filter @storyblok/management-api-client test:e2e
|
|
13
|
+
*
|
|
14
|
+
* Requires .env.qa-engineer-manual at the repo root with:
|
|
15
|
+
* STORYBLOK_TOKEN=<personal-access-token>
|
|
16
|
+
* STORYBLOK_SPACE_ID=<numeric-space-id>
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
20
|
+
import { createManagementApiClient } from '@storyblok/management-api-client';
|
|
21
|
+
import {
|
|
22
|
+
createStoryHelpers,
|
|
23
|
+
defineBlock,
|
|
24
|
+
defineBlockCreate,
|
|
25
|
+
defineBlockFolderCreate,
|
|
26
|
+
defineDatasourceCreate,
|
|
27
|
+
defineDatasourceEntryCreate,
|
|
28
|
+
defineField,
|
|
29
|
+
defineInternalTagCreate,
|
|
30
|
+
definePresetCreate,
|
|
31
|
+
} from '@storyblok/schema';
|
|
32
|
+
|
|
33
|
+
const token = process.env.STORYBLOK_TOKEN!;
|
|
34
|
+
const spaceId = Number(process.env.STORYBLOK_SPACE_ID!);
|
|
35
|
+
|
|
36
|
+
const PREFIX = 'e2e_schema_';
|
|
37
|
+
const STORY_SLUG_PREFIX = 'e2e-schema-';
|
|
38
|
+
|
|
39
|
+
const DATASOURCE_SLUG = 'e2e-schema-categories';
|
|
40
|
+
const DATASOURCE_NAME = `${PREFIX}categories`;
|
|
41
|
+
const STORY_NAME = 'E2E Schema Test Page';
|
|
42
|
+
const STORY_SLUG = `${STORY_SLUG_PREFIX}test-page`;
|
|
43
|
+
|
|
44
|
+
const teaserComponent = defineBlock({
|
|
45
|
+
name: `${PREFIX}teaser`,
|
|
46
|
+
schema: [
|
|
47
|
+
defineField('title', { type: 'text', required: true }),
|
|
48
|
+
defineField('image', { type: 'asset' }),
|
|
49
|
+
],
|
|
50
|
+
});
|
|
51
|
+
// Level-2 container: holds teasers in its `items` bloks field (level 3)
|
|
52
|
+
const sectionComponent = defineBlock({
|
|
53
|
+
name: `${PREFIX}section`,
|
|
54
|
+
schema: [
|
|
55
|
+
defineField('title', { type: 'text' }),
|
|
56
|
+
defineField('items', { type: 'bloks', component_whitelist: [teaserComponent.name], required: true }),
|
|
57
|
+
],
|
|
58
|
+
});
|
|
59
|
+
const pageComponent = defineBlock({
|
|
60
|
+
name: `${PREFIX}page`,
|
|
61
|
+
is_root: true,
|
|
62
|
+
schema: [
|
|
63
|
+
defineField('headline', { type: 'text', required: true }),
|
|
64
|
+
defineField('rating', { type: 'number' }),
|
|
65
|
+
defineField('is_featured', { type: 'boolean' }),
|
|
66
|
+
defineField('description', { type: 'richtext' }),
|
|
67
|
+
defineField('body', { type: 'bloks', component_whitelist: [teaserComponent.name, sectionComponent.name], required: true }),
|
|
68
|
+
defineField('category', { type: 'option', source: 'internal', datasource_slug: DATASOURCE_SLUG }),
|
|
69
|
+
defineField('any_blocks', { type: 'bloks', required: true }),
|
|
70
|
+
],
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
interface StoryblokTypes {
|
|
74
|
+
components: typeof pageComponent | typeof teaserComponent | typeof sectionComponent;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const { defineStoryCreate, defineStoryUpdate } = createStoryHelpers().withTypes<StoryblokTypes>();
|
|
78
|
+
|
|
79
|
+
const client = createManagementApiClient({
|
|
80
|
+
personalAccessToken: token,
|
|
81
|
+
spaceId,
|
|
82
|
+
throwOnError: true,
|
|
83
|
+
}).withTypes<StoryblokTypes>();
|
|
84
|
+
|
|
85
|
+
async function cleanup() {
|
|
86
|
+
// Stories
|
|
87
|
+
const storiesRes = await client.stories.list({ query: { per_page: 100 } });
|
|
88
|
+
for (const story of storiesRes.data?.stories ?? []) {
|
|
89
|
+
if (story.slug?.startsWith(STORY_SLUG_PREFIX) && story.id) {
|
|
90
|
+
await client.stories.delete(story.id);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Presets
|
|
95
|
+
const presetsRes = await client.presets.list();
|
|
96
|
+
for (const preset of presetsRes.data?.presets ?? []) {
|
|
97
|
+
if (preset.name?.startsWith(PREFIX) && preset.id) {
|
|
98
|
+
await client.presets.delete(preset.id);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Internal tags
|
|
103
|
+
const tagsRes = await client.internalTags.list();
|
|
104
|
+
for (const tag of tagsRes.data?.internal_tags ?? []) {
|
|
105
|
+
if (tag.name?.startsWith(PREFIX) && tag.id) {
|
|
106
|
+
await client.internalTags.delete(tag.id);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Components
|
|
111
|
+
const compsRes = await client.components.list();
|
|
112
|
+
for (const comp of compsRes.data?.components ?? []) {
|
|
113
|
+
if (comp.name?.startsWith(PREFIX) && comp.id) {
|
|
114
|
+
await client.components.delete(comp.id);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Component folders
|
|
119
|
+
const foldersRes = await client.componentFolders.list();
|
|
120
|
+
for (const folder of foldersRes.data?.component_groups ?? []) {
|
|
121
|
+
if (folder.name?.startsWith(PREFIX) && folder.id) {
|
|
122
|
+
await client.componentFolders.delete(folder.id);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Datasource entries, then datasources
|
|
127
|
+
const dsRes = await client.datasources.list();
|
|
128
|
+
for (const ds of dsRes.data?.datasources ?? []) {
|
|
129
|
+
if (ds.name?.startsWith(PREFIX) && ds.id) {
|
|
130
|
+
const entriesRes = await client.datasourceEntries.list({
|
|
131
|
+
query: { datasource_id: ds.id, per_page: 100 },
|
|
132
|
+
});
|
|
133
|
+
for (const entry of entriesRes.data?.datasource_entries ?? []) {
|
|
134
|
+
if (entry.id) {
|
|
135
|
+
await client.datasourceEntries.delete(entry.id);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
await client.datasources.delete(ds.id);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
describe('schema + mapi-client MAPI round-trip', () => {
|
|
144
|
+
let pageComponentId: number;
|
|
145
|
+
let teaserComponentId: number;
|
|
146
|
+
let sectionComponentId: number;
|
|
147
|
+
let componentFolderId: number;
|
|
148
|
+
let datasourceId: number;
|
|
149
|
+
let internalTagId: number;
|
|
150
|
+
let presetId: number;
|
|
151
|
+
let storyId: number;
|
|
152
|
+
|
|
153
|
+
beforeAll(async () => {
|
|
154
|
+
await cleanup();
|
|
155
|
+
|
|
156
|
+
// 1. Datasource + entries first — the page component schema references the datasource slug,
|
|
157
|
+
// and the MAPI validates that the datasource exists at component creation time.
|
|
158
|
+
const datasourcePayload = defineDatasourceCreate({
|
|
159
|
+
name: DATASOURCE_NAME,
|
|
160
|
+
slug: DATASOURCE_SLUG,
|
|
161
|
+
});
|
|
162
|
+
const dsRes = await client.datasources.create({ body: { datasource: datasourcePayload } });
|
|
163
|
+
datasourceId = dsRes.data!.datasource!.id!;
|
|
164
|
+
|
|
165
|
+
for (const entry of [
|
|
166
|
+
defineDatasourceEntryCreate({ name: 'Technology', value: 'tech', datasource_id: datasourceId }),
|
|
167
|
+
defineDatasourceEntryCreate({ name: 'Design', value: 'design', datasource_id: datasourceId }),
|
|
168
|
+
defineDatasourceEntryCreate({ name: 'Business', value: 'business', datasource_id: datasourceId }),
|
|
169
|
+
]) {
|
|
170
|
+
await client.datasourceEntries.create({ body: { datasource_entry: entry } });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 2. Component folder
|
|
174
|
+
const folderPayload = defineBlockFolderCreate({ name: `${PREFIX}folder` });
|
|
175
|
+
const folderRes = await client.componentFolders.create({ body: { component_group: folderPayload } });
|
|
176
|
+
componentFolderId = folderRes.data!.component_group!.id!;
|
|
177
|
+
const folderUuid = folderRes.data!.component_group!.uuid;
|
|
178
|
+
|
|
179
|
+
// 3. Teaser component (innermost — whitelisted by section)
|
|
180
|
+
const teaserPayload = defineBlockCreate({
|
|
181
|
+
name: teaserComponent.name,
|
|
182
|
+
schema: {
|
|
183
|
+
title: { type: 'text', required: true, pos: 0 },
|
|
184
|
+
image: { type: 'asset', pos: 1 },
|
|
185
|
+
},
|
|
186
|
+
component_group_uuid: folderUuid,
|
|
187
|
+
});
|
|
188
|
+
const teaserRes = await client.components.create({ body: { component: teaserPayload } });
|
|
189
|
+
teaserComponentId = teaserRes.data!.component!.id!;
|
|
190
|
+
|
|
191
|
+
// 4. Section component (level 2 — whitelists teaser, whitelisted by page)
|
|
192
|
+
const sectionPayload = defineBlockCreate({
|
|
193
|
+
name: sectionComponent.name,
|
|
194
|
+
schema: {
|
|
195
|
+
title: { type: 'text', pos: 0 },
|
|
196
|
+
items: { type: 'bloks', component_whitelist: [teaserComponent.name], pos: 1 },
|
|
197
|
+
},
|
|
198
|
+
component_group_uuid: folderUuid,
|
|
199
|
+
});
|
|
200
|
+
const sectionRes = await client.components.create({ body: { component: sectionPayload } });
|
|
201
|
+
sectionComponentId = sectionRes.data!.component!.id!;
|
|
202
|
+
|
|
203
|
+
// 5. Page component (level 1 — whitelists both teaser and section in body)
|
|
204
|
+
const pagePayload = defineBlockCreate({
|
|
205
|
+
name: pageComponent.name,
|
|
206
|
+
schema: {
|
|
207
|
+
headline: { type: 'text', required: true, pos: 0 },
|
|
208
|
+
rating: { type: 'number', pos: 1 },
|
|
209
|
+
is_featured: { type: 'boolean', pos: 2 },
|
|
210
|
+
description: { type: 'richtext', pos: 3 },
|
|
211
|
+
body: { type: 'bloks', component_whitelist: [teaserComponent.name, sectionComponent.name], pos: 4 },
|
|
212
|
+
category: { type: 'option', source: 'internal', datasource_slug: DATASOURCE_SLUG, pos: 5 },
|
|
213
|
+
any_blocks: { type: 'bloks', pos: 6 },
|
|
214
|
+
},
|
|
215
|
+
component_group_uuid: folderUuid,
|
|
216
|
+
is_root: true,
|
|
217
|
+
});
|
|
218
|
+
const pageRes = await client.components.create({ body: { component: pagePayload } });
|
|
219
|
+
pageComponentId = pageRes.data!.component!.id!;
|
|
220
|
+
|
|
221
|
+
// 5. Internal tag
|
|
222
|
+
const tagPayload = defineInternalTagCreate({ name: `${PREFIX}tag`, object_type: 'component' });
|
|
223
|
+
const tagRes = await client.internalTags.create({ body: { internal_tag: tagPayload } });
|
|
224
|
+
internalTagId = tagRes.data!.internal_tag!.id!;
|
|
225
|
+
|
|
226
|
+
// 6. Preset for page component
|
|
227
|
+
const presetPayload = definePresetCreate({
|
|
228
|
+
name: `${PREFIX}default_page`,
|
|
229
|
+
component_id: pageComponentId,
|
|
230
|
+
preset: { headline: 'Default Headline', rating: 0, is_featured: false },
|
|
231
|
+
description: `Default preset for ${pageComponent.name}`,
|
|
232
|
+
});
|
|
233
|
+
const presetRes = await client.presets.create({ body: { preset: presetPayload } });
|
|
234
|
+
presetId = presetRes.data!.preset!.id!;
|
|
235
|
+
|
|
236
|
+
// 8. Story: body[0]=teaser (level 2), body[1]=section{items:[teaser]} (levels 2+3)
|
|
237
|
+
const storyPayload = defineStoryCreate(pageComponent, {
|
|
238
|
+
name: STORY_NAME,
|
|
239
|
+
slug: STORY_SLUG,
|
|
240
|
+
content: {
|
|
241
|
+
headline: 'Hello from e2e',
|
|
242
|
+
rating: 42,
|
|
243
|
+
is_featured: true,
|
|
244
|
+
description: {
|
|
245
|
+
type: 'doc',
|
|
246
|
+
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Rich text body.' }] }],
|
|
247
|
+
},
|
|
248
|
+
body: [
|
|
249
|
+
{
|
|
250
|
+
component: teaserComponent.name,
|
|
251
|
+
title: 'Teaser Title',
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
component: sectionComponent.name,
|
|
255
|
+
title: 'Section Title',
|
|
256
|
+
items: [
|
|
257
|
+
{
|
|
258
|
+
component: teaserComponent.name,
|
|
259
|
+
title: 'Nested Teaser Title',
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
category: 'tech',
|
|
265
|
+
any_blocks: [
|
|
266
|
+
{
|
|
267
|
+
component: teaserComponent.name,
|
|
268
|
+
title: 'Any Block Teaser',
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
const storyRes = await client.stories.create({ body: { story: storyPayload } });
|
|
274
|
+
storyId = storyRes.data!.story!.id!;
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
afterAll(cleanup);
|
|
278
|
+
|
|
279
|
+
describe('components', () => {
|
|
280
|
+
it('should create page component with the correct name and schema keys', async () => {
|
|
281
|
+
const res = await client.components.get(pageComponentId);
|
|
282
|
+
const comp = res.data?.component;
|
|
283
|
+
|
|
284
|
+
expect(comp).toBeDefined();
|
|
285
|
+
expect(comp?.name).toBe(pageComponent.name);
|
|
286
|
+
expect(comp?.schema).toMatchObject({
|
|
287
|
+
headline: expect.objectContaining({ type: 'text' }),
|
|
288
|
+
rating: expect.objectContaining({ type: 'number' }),
|
|
289
|
+
is_featured: expect.objectContaining({ type: 'boolean' }),
|
|
290
|
+
description: expect.objectContaining({ type: 'richtext' }),
|
|
291
|
+
body: expect.objectContaining({ type: 'bloks' }),
|
|
292
|
+
category: expect.objectContaining({ type: 'option' }),
|
|
293
|
+
});
|
|
294
|
+
expect(comp?.name).toBe(pageComponent.name);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should create teaser component with the correct schema', async () => {
|
|
298
|
+
const res = await client.components.get(teaserComponentId);
|
|
299
|
+
const comp = res.data?.component;
|
|
300
|
+
|
|
301
|
+
expect(comp?.name).toBe(teaserComponent.name);
|
|
302
|
+
expect(comp?.schema).toMatchObject({
|
|
303
|
+
title: expect.objectContaining({ type: 'text', required: true }),
|
|
304
|
+
image: expect.objectContaining({ type: 'asset' }),
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('should create section component with the correct schema', async () => {
|
|
309
|
+
const res = await client.components.get(sectionComponentId);
|
|
310
|
+
const comp = res.data?.component;
|
|
311
|
+
|
|
312
|
+
expect(comp?.name).toBe(sectionComponent.name);
|
|
313
|
+
expect(comp?.schema).toMatchObject({
|
|
314
|
+
title: expect.objectContaining({ type: 'text' }),
|
|
315
|
+
items: expect.objectContaining({ type: 'bloks', component_whitelist: [teaserComponent.name] }),
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('should assign components to the correct component folder', async () => {
|
|
320
|
+
const folderRes = await client.componentFolders.get(componentFolderId);
|
|
321
|
+
const folder = folderRes.data?.component_group;
|
|
322
|
+
|
|
323
|
+
const pageRes = await client.components.get(pageComponentId);
|
|
324
|
+
const comp = pageRes.data?.component;
|
|
325
|
+
|
|
326
|
+
expect(folder?.name).toBe(`${PREFIX}folder`);
|
|
327
|
+
expect(comp?.component_group_uuid).toBe(folder?.uuid);
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
describe('datasources', () => {
|
|
332
|
+
it('should create datasource with the correct name and slug', async () => {
|
|
333
|
+
const res = await client.datasources.list();
|
|
334
|
+
const ds = res.data?.datasources?.find(d => d.id === datasourceId);
|
|
335
|
+
|
|
336
|
+
expect(ds).toBeDefined();
|
|
337
|
+
expect(ds?.name).toBe(DATASOURCE_NAME);
|
|
338
|
+
expect(ds?.slug).toBe(DATASOURCE_SLUG);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('should create datasource entries with the correct names and values', async () => {
|
|
342
|
+
const res = await client.datasourceEntries.list({
|
|
343
|
+
query: { datasource_id: datasourceId, per_page: 100 },
|
|
344
|
+
});
|
|
345
|
+
const entries = res.data?.datasource_entries ?? [];
|
|
346
|
+
|
|
347
|
+
expect(entries.length).toBeGreaterThanOrEqual(3);
|
|
348
|
+
expect(entries.find(e => e.value === 'tech')?.name).toBe('Technology');
|
|
349
|
+
expect(entries.find(e => e.value === 'design')?.name).toBe('Design');
|
|
350
|
+
expect(entries.find(e => e.value === 'business')?.name).toBe('Business');
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
describe('internalTags', () => {
|
|
355
|
+
it('should create and retrieve internal tag', async () => {
|
|
356
|
+
const res = await client.internalTags.list();
|
|
357
|
+
const tag = res.data?.internal_tags?.find(t => t.id === internalTagId);
|
|
358
|
+
|
|
359
|
+
expect(tag).toBeDefined();
|
|
360
|
+
expect(tag?.name).toBe(`${PREFIX}tag`);
|
|
361
|
+
expect(tag?.object_type).toBe('component');
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
describe('presets', () => {
|
|
366
|
+
it('should create preset with the correct component_id and default values', async () => {
|
|
367
|
+
const res = await client.presets.get(presetId);
|
|
368
|
+
const preset = res.data?.preset;
|
|
369
|
+
|
|
370
|
+
expect(preset).toBeDefined();
|
|
371
|
+
expect(preset?.name).toBe(`${PREFIX}default_page`);
|
|
372
|
+
expect(preset?.component_id).toBe(pageComponentId);
|
|
373
|
+
expect(preset?.preset).toMatchObject({
|
|
374
|
+
headline: 'Default Headline',
|
|
375
|
+
rating: 0,
|
|
376
|
+
is_featured: false,
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
describe('stories', () => {
|
|
382
|
+
it('should match defined schema field types at runtime for story content', async () => {
|
|
383
|
+
const res = await client.stories.get(storyId);
|
|
384
|
+
const story = res.data?.story;
|
|
385
|
+
|
|
386
|
+
expect(story).toBeDefined();
|
|
387
|
+
expect(story?.name).toBe(STORY_NAME);
|
|
388
|
+
expect(story?.slug).toBe(STORY_SLUG);
|
|
389
|
+
|
|
390
|
+
// TypeScript narrows story.content to the pageComponent branch here.
|
|
391
|
+
// Accessing story.content.headline etc. only compiles when the component guard passes,
|
|
392
|
+
// so this block validates both runtime correctness and compile-time type narrowing.
|
|
393
|
+
if (story?.content?.component === pageComponent.name) {
|
|
394
|
+
expect(typeof story.content.headline).toBe('string');
|
|
395
|
+
expect(story.content.headline).toBe('Hello from e2e');
|
|
396
|
+
|
|
397
|
+
expect(typeof story.content.rating).toBe('number');
|
|
398
|
+
expect(story.content.rating).toBe(42);
|
|
399
|
+
|
|
400
|
+
expect(typeof story.content.is_featured).toBe('boolean');
|
|
401
|
+
expect(story.content.is_featured).toBe(true);
|
|
402
|
+
|
|
403
|
+
expect(story.content.description).toMatchObject({ type: 'doc' });
|
|
404
|
+
|
|
405
|
+
expect(Array.isArray(story.content.body)).toBe(true);
|
|
406
|
+
expect(story.content.body.length).toBeGreaterThan(0);
|
|
407
|
+
|
|
408
|
+
expect(typeof story.content.category).toBe('string');
|
|
409
|
+
expect(story.content.category).toBe('tech');
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
throw new Error(
|
|
413
|
+
`Expected story.content.component to be '${pageComponent.name}', got '${story?.content?.component}'`,
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('should have correct structure for nested teaser blok (two levels: page → teaser)', async () => {
|
|
419
|
+
const res = await client.stories.get(storyId);
|
|
420
|
+
const story = res.data?.story;
|
|
421
|
+
|
|
422
|
+
if (story?.content?.component !== pageComponent.name) {
|
|
423
|
+
throw new Error('Unexpected component discriminant');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const teaser = story.content.body[0];
|
|
427
|
+
|
|
428
|
+
expect(teaser).toBeDefined();
|
|
429
|
+
expect(teaser?.component).toBe(teaserComponent.name);
|
|
430
|
+
expect(typeof teaser?.title).toBe('string');
|
|
431
|
+
expect(teaser?.title).toBe('Teaser Title');
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('should resolve correct types for three-level nested blok (page → section → teaser)', async () => {
|
|
435
|
+
const res = await client.stories.get(storyId);
|
|
436
|
+
const story = res.data?.story;
|
|
437
|
+
|
|
438
|
+
if (story?.content?.component !== pageComponent.name) {
|
|
439
|
+
throw new Error('Unexpected component discriminant at level 1');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Level 2: narrow body[1] to section
|
|
443
|
+
const section = story.content.body[1];
|
|
444
|
+
if (section?.component === sectionComponent.name) {
|
|
445
|
+
expect(typeof section.title).toBe('string');
|
|
446
|
+
expect(section.title).toBe('Section Title');
|
|
447
|
+
expect(Array.isArray(section.items)).toBe(true);
|
|
448
|
+
expect(section.items.length).toBeGreaterThan(0);
|
|
449
|
+
|
|
450
|
+
// Level 3: section.items[0] is a typed teaser — not `never`
|
|
451
|
+
const nestedTeaser = section.items[0];
|
|
452
|
+
if (nestedTeaser?.component === teaserComponent.name) {
|
|
453
|
+
expect(typeof nestedTeaser.title).toBe('string');
|
|
454
|
+
expect(nestedTeaser.title).toBe('Nested Teaser Title');
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
throw new Error(`Expected items[0].component to be '${teaserComponent.name}', got '${nestedTeaser?.component}'`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
throw new Error(`Expected body[1].component to be '${sectionComponent.name}', got '${section?.component}'`);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it('should resolve all schema component types for bloks field without whitelist', async () => {
|
|
466
|
+
const res = await client.stories.get(storyId);
|
|
467
|
+
const story = res.data?.story;
|
|
468
|
+
|
|
469
|
+
if (story?.content?.component !== pageComponent.name) {
|
|
470
|
+
throw new Error('Unexpected component discriminant');
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// any_blocks has no component_whitelist — TypeScript resolves items to the full
|
|
474
|
+
// schema union (page | teaser | section), not `never` and not a generic block
|
|
475
|
+
// with only component: string. Accessing blok.title after narrowing to the
|
|
476
|
+
// teaser discriminant proves the discriminated union is correctly typed.
|
|
477
|
+
const blok = story.content.any_blocks[0];
|
|
478
|
+
if (blok?.component === teaserComponent.name) {
|
|
479
|
+
expect(typeof blok.title).toBe('string');
|
|
480
|
+
expect(blok.title).toBe('Any Block Teaser');
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
throw new Error(`Expected any_blocks[0].component to be '${teaserComponent.name}', got '${blok?.component}'`);
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it('should round-trip story update correctly', async () => {
|
|
488
|
+
const updatedPayload = defineStoryUpdate(pageComponent, {
|
|
489
|
+
name: `${STORY_NAME} (Updated)`,
|
|
490
|
+
slug: STORY_SLUG,
|
|
491
|
+
content: {
|
|
492
|
+
headline: 'Updated headline',
|
|
493
|
+
rating: 100,
|
|
494
|
+
is_featured: false,
|
|
495
|
+
description: {
|
|
496
|
+
type: 'doc',
|
|
497
|
+
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Updated.' }] }],
|
|
498
|
+
},
|
|
499
|
+
body: [],
|
|
500
|
+
category: 'design',
|
|
501
|
+
any_blocks: [],
|
|
502
|
+
},
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
await client.stories.update(storyId, { body: { story: updatedPayload } });
|
|
506
|
+
|
|
507
|
+
const res = await client.stories.get(storyId);
|
|
508
|
+
const story = res.data?.story;
|
|
509
|
+
|
|
510
|
+
if (story?.content?.component !== pageComponent.name) {
|
|
511
|
+
throw new Error('Unexpected component discriminant after update');
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
expect(story.content.headline).toBe('Updated headline');
|
|
515
|
+
expect(story.content.rating).toBe(100);
|
|
516
|
+
expect(story.content.is_featured).toBe(false);
|
|
517
|
+
expect(story.content.category).toBe('design');
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/// <reference types="vitest" />
|
|
2
|
+
import { defineConfig } from 'vitest/config';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Vitest config for end-to-end tests that hit the real Storyblok Management API.
|
|
6
|
+
*
|
|
7
|
+
* These tests are NOT run in CI. Trigger them manually:
|
|
8
|
+
* pnpm --filter @storyblok/management-api-client test:e2e
|
|
9
|
+
*
|
|
10
|
+
* Prerequisites:
|
|
11
|
+
* - A .env.qa-engineer-manual file at the repo root with STORYBLOK_TOKEN and STORYBLOK_SPACE_ID.
|
|
12
|
+
* - The @storyblok/management-api-client package must be built.
|
|
13
|
+
*/
|
|
14
|
+
export default defineConfig({
|
|
15
|
+
test: {
|
|
16
|
+
include: ['./test/specs/**/*.spec.e2e.ts'],
|
|
17
|
+
setupFiles: ['./test/setup.e2e.ts'],
|
|
18
|
+
globals: true,
|
|
19
|
+
testTimeout: 60_000,
|
|
20
|
+
hookTimeout: 120_000,
|
|
21
|
+
// Run tests sequentially to avoid MAPI rate-limit issues
|
|
22
|
+
sequence: {
|
|
23
|
+
concurrent: false,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
});
|
package/vitest.config.ts
CHANGED
package/dist/index.cjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["createThrottleManager","createClient","createConfig","ClientError","createAssetFoldersResource","createAssetsResource","createComponentFoldersResource","createComponentsResource","createDatasourceEntriesResource","createDatasourcesResource","createInternalTagsResource","createPresetsResource","createSpacesResource","createStoriesResource","createUsersResource"],"sources":["../src/index.ts"],"sourcesContent":["import type { Client, ResolvedRequestOptions } from './generated/shared/client';\nimport type { Middleware } from './generated/shared/client/utils.gen';\nimport { createClient, createConfig } from './generated/shared/client';\nimport { getManagementBaseUrl } from '@storyblok/region-helper';\nimport { ClientError } from './error';\nimport { createThrottleManager } from './utils/rate-limit';\nimport { createAssetFoldersResource } from './resources/asset-folders';\nimport { createAssetsResource } from './resources/assets';\nimport { createComponentFoldersResource } from './resources/component-folders';\nimport { createComponentsResource } from './resources/components';\nimport { createDatasourceEntriesResource } from './resources/datasource-entries';\nimport { createDatasourcesResource } from './resources/datasources';\nimport { createInternalTagsResource } from './resources/internal-tags';\nimport { createPresetsResource } from './resources/presets';\nimport { createSpacesResource } from './resources/spaces';\nimport { createStoriesResource } from './resources/stories';\nimport { createUsersResource } from './resources/users';\nimport type {\n ApiResponse,\n HttpRequestOptions,\n ManagementApiClientConfig,\n MapiResourceDeps,\n} from './types';\n\nexport { ClientError } from './error';\n\nfunction getAuthorizationHeader(config: ManagementApiClientConfig): string | undefined {\n if (config.personalAccessToken) {\n return config.personalAccessToken;\n }\n if (config.oauthToken) {\n return `Bearer ${config.oauthToken.replace(/^Bearer\\s+/i, '')}`;\n }\n return undefined;\n}\n\nexport const createManagementApiClient = (config: ManagementApiClientConfig) => {\n const {\n spaceId,\n region = 'eu',\n baseUrl,\n headers = {},\n throwOnError = false,\n retry = {\n limit: 12,\n backoffLimit: 20_000,\n methods: ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace'],\n statusCodes: [429],\n },\n timeout = 30_000,\n rateLimit,\n } = config;\n\n const throttleManager = createThrottleManager(rateLimit ?? {});\n const authHeader = getAuthorizationHeader(config);\n\n const client: Client = createClient(\n createConfig({\n baseUrl: baseUrl || getManagementBaseUrl(region),\n headers: {\n ...(authHeader ? { Authorization: authHeader } : {}),\n ...headers,\n },\n throwOnError,\n kyOptions: {\n throwHttpErrors: true,\n timeout,\n retry,\n },\n }),\n );\n\n client.interceptors.error.use(\n (error: unknown, response: Response) =>\n new ClientError(response?.statusText || 'API request failed', {\n status: response?.status ?? 0,\n statusText: response?.statusText ?? '',\n data: error,\n }),\n );\n\n /**\n * Wraps an SDK call with throttling and response adaptation.\n * The throttle slot is held for the entire duration of the request.\n * When throwOnError is true, errors throw and data is guaranteed non-null.\n */\n function wrapRequest<TData, ThrowOnError extends boolean = false>(\n fn: () => Promise<unknown>,\n _throwOnError?: ThrowOnError,\n ): Promise<ApiResponse<TData, ThrowOnError>> {\n return throttleManager.execute(async () => {\n const result = await fn() as ApiResponse<TData, ThrowOnError>;\n throttleManager.adaptToResponse((result as { response: Response }).response);\n return result;\n }) as Promise<ApiResponse<TData, ThrowOnError>>;\n }\n\n const deps: MapiResourceDeps = { client, spaceId, wrapRequest };\n\n /**\n * Escape hatch: send a GET request to any MAPI endpoint not yet wrapped\n * in a dedicated resource method.\n */\n const httpGet = <TData = unknown>(\n path: string,\n options: HttpRequestOptions = {},\n ): Promise<ApiResponse<TData>> => {\n const { fetchOptions, ...rest } = options;\n return wrapRequest<TData>(() =>\n client.get({ url: path, ...rest, ...(fetchOptions ? { kyOptions: { ...client.getConfig().kyOptions, ...fetchOptions } } : {}) }),\n );\n };\n\n /**\n * Escape hatch: send a POST request to any MAPI endpoint not yet wrapped\n * in a dedicated resource method.\n */\n const httpPost = <TData = unknown>(\n path: string,\n options: HttpRequestOptions = {},\n ): Promise<ApiResponse<TData>> => {\n const { fetchOptions, ...rest } = options;\n return wrapRequest<TData>(() =>\n client.post({ url: path, ...rest, ...(fetchOptions ? { kyOptions: { ...client.getConfig().kyOptions, ...fetchOptions } } : {}) }),\n );\n };\n\n /**\n * Escape hatch: send a PUT request to any MAPI endpoint not yet wrapped\n * in a dedicated resource method.\n */\n const httpPut = <TData = unknown>(\n path: string,\n options: HttpRequestOptions = {},\n ): Promise<ApiResponse<TData>> => {\n const { fetchOptions, ...rest } = options;\n return wrapRequest<TData>(() =>\n client.put({ url: path, ...rest, ...(fetchOptions ? { kyOptions: { ...client.getConfig().kyOptions, ...fetchOptions } } : {}) }),\n );\n };\n\n /**\n * Escape hatch: send a PATCH request to any MAPI endpoint not yet wrapped\n * in a dedicated resource method.\n */\n const httpPatch = <TData = unknown>(\n path: string,\n options: HttpRequestOptions = {},\n ): Promise<ApiResponse<TData>> => {\n const { fetchOptions, ...rest } = options;\n return wrapRequest<TData>(() =>\n client.patch({ url: path, ...rest, ...(fetchOptions ? { kyOptions: { ...client.getConfig().kyOptions, ...fetchOptions } } : {}) }),\n );\n };\n\n /**\n * Escape hatch: send a DELETE request to any MAPI endpoint not yet wrapped\n * in a dedicated resource method.\n */\n const httpDelete = <TData = unknown>(\n path: string,\n options: HttpRequestOptions = {},\n ): Promise<ApiResponse<TData>> => {\n const { fetchOptions, ...rest } = options;\n return wrapRequest<TData>(() =>\n client.delete({ url: path, ...rest, ...(fetchOptions ? { kyOptions: { ...client.getConfig().kyOptions, ...fetchOptions } } : {}) }),\n );\n };\n\n return {\n assetFolders: createAssetFoldersResource(deps),\n assets: createAssetsResource(deps),\n componentFolders: createComponentFoldersResource(deps),\n components: createComponentsResource(deps),\n datasourceEntries: createDatasourceEntriesResource(deps),\n datasources: createDatasourcesResource(deps),\n delete: httpDelete,\n get: httpGet,\n patch: httpPatch,\n interceptors: client.interceptors as Middleware<Request, Response, unknown, ResolvedRequestOptions>,\n internalTags: createInternalTagsResource(deps),\n post: httpPost,\n presets: createPresetsResource(deps),\n put: httpPut,\n spaces: createSpacesResource(deps),\n stories: createStoriesResource(deps),\n users: createUsersResource({ client, wrapRequest }),\n };\n};\n\nexport type ManagementApiClient = ReturnType<typeof createManagementApiClient>;\n\nexport type {\n ApiResponse,\n Asset,\n AssetCreate,\n AssetField,\n AssetFolder,\n AssetFolderCreate,\n AssetFolderUpdate,\n AssetListQuery,\n AssetSignRequest,\n AssetUpdate,\n AssetUpdateRequest,\n AssetUploadRequest,\n Component,\n ComponentCreate,\n ComponentFolder,\n ComponentSchemaField,\n ComponentUpdate,\n Datasource,\n DatasourceCreate,\n DatasourceEntry,\n DatasourceEntryCreate,\n DatasourceEntryUpdate,\n DatasourceUpdate,\n FetchOptions,\n HttpRequestOptions,\n InternalTag,\n ManagementApiClientConfig,\n MapiResourceDeps,\n MultilinkField,\n PluginField,\n Preset,\n RateLimitConfig,\n RequestConfigOverrides,\n RichtextField,\n SignedResponseObject,\n Space,\n SpaceCreate,\n SpaceUpdate,\n Story,\n StoryAlternate,\n StoryContent,\n StoryCreate,\n StoryListQuery,\n StoryLocalizedPath,\n StoryTranslatedSlug,\n StoryUpdate,\n TableField,\n User,\n} from './types';\nexport { normalizeAssetUrl } from './utils/normalize-asset-url';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA0BA,SAAS,uBAAuB,QAAuD;AACrF,KAAI,OAAO,oBACT,QAAO,OAAO;AAEhB,KAAI,OAAO,WACT,QAAO,UAAU,OAAO,WAAW,QAAQ,eAAe,GAAG;;AAKjE,MAAa,6BAA6B,WAAsC;CAC9E,MAAM,EACJ,SACA,SAAS,MACT,SACA,UAAU,EAAE,EACZ,eAAe,OACf,QAAQ;EACN,OAAO;EACP,cAAc;EACd,SAAS;GAAC;GAAO;GAAQ;GAAO;GAAU;GAAS;GAAQ;GAAW;GAAQ;EAC9E,aAAa,CAAC,IAAI;EACnB,EACD,UAAU,KACV,cACE;CAEJ,MAAM,kBAAkBA,yCAAsB,aAAa,EAAE,CAAC;CAC9D,MAAM,aAAa,uBAAuB,OAAO;CAEjD,MAAM,SAAiBC,gCACrBC,+BAAa;EACX,SAAS,8DAAgC,OAAO;EAChD,SAAS;GACP,GAAI,aAAa,EAAE,eAAe,YAAY,GAAG,EAAE;GACnD,GAAG;GACJ;EACD;EACA,WAAW;GACT,iBAAiB;GACjB;GACA;GACD;EACF,CAAC,CACH;AAED,QAAO,aAAa,MAAM,KACvB,OAAgB,aACf,IAAIC,0BAAY,UAAU,cAAc,sBAAsB;EAC5D,QAAQ,UAAU,UAAU;EAC5B,YAAY,UAAU,cAAc;EACpC,MAAM;EACP,CAAC,CACL;;;;;;CAOD,SAAS,YACP,IACA,eAC2C;AAC3C,SAAO,gBAAgB,QAAQ,YAAY;GACzC,MAAM,SAAS,MAAM,IAAI;AACzB,mBAAgB,gBAAiB,OAAkC,SAAS;AAC5E,UAAO;IACP;;CAGJ,MAAM,OAAyB;EAAE;EAAQ;EAAS;EAAa;;;;;CAM/D,MAAM,WACJ,MACA,UAA8B,EAAE,KACA;EAChC,MAAM,EAAE,cAAc,GAAG,SAAS;AAClC,SAAO,kBACL,OAAO,IAAI;GAAE,KAAK;GAAM,GAAG;GAAM,GAAI,eAAe,EAAE,WAAW;IAAE,GAAG,OAAO,WAAW,CAAC;IAAW,GAAG;IAAc,EAAE,GAAG,EAAE;GAAG,CAAC,CACjI;;;;;;CAOH,MAAM,YACJ,MACA,UAA8B,EAAE,KACA;EAChC,MAAM,EAAE,cAAc,GAAG,SAAS;AAClC,SAAO,kBACL,OAAO,KAAK;GAAE,KAAK;GAAM,GAAG;GAAM,GAAI,eAAe,EAAE,WAAW;IAAE,GAAG,OAAO,WAAW,CAAC;IAAW,GAAG;IAAc,EAAE,GAAG,EAAE;GAAG,CAAC,CAClI;;;;;;CAOH,MAAM,WACJ,MACA,UAA8B,EAAE,KACA;EAChC,MAAM,EAAE,cAAc,GAAG,SAAS;AAClC,SAAO,kBACL,OAAO,IAAI;GAAE,KAAK;GAAM,GAAG;GAAM,GAAI,eAAe,EAAE,WAAW;IAAE,GAAG,OAAO,WAAW,CAAC;IAAW,GAAG;IAAc,EAAE,GAAG,EAAE;GAAG,CAAC,CACjI;;;;;;CAOH,MAAM,aACJ,MACA,UAA8B,EAAE,KACA;EAChC,MAAM,EAAE,cAAc,GAAG,SAAS;AAClC,SAAO,kBACL,OAAO,MAAM;GAAE,KAAK;GAAM,GAAG;GAAM,GAAI,eAAe,EAAE,WAAW;IAAE,GAAG,OAAO,WAAW,CAAC;IAAW,GAAG;IAAc,EAAE,GAAG,EAAE;GAAG,CAAC,CACnI;;;;;;CAOH,MAAM,cACJ,MACA,UAA8B,EAAE,KACA;EAChC,MAAM,EAAE,cAAc,GAAG,SAAS;AAClC,SAAO,kBACL,OAAO,OAAO;GAAE,KAAK;GAAM,GAAG;GAAM,GAAI,eAAe,EAAE,WAAW;IAAE,GAAG,OAAO,WAAW,CAAC;IAAW,GAAG;IAAc,EAAE,GAAG,EAAE;GAAG,CAAC,CACpI;;AAGH,QAAO;EACL,cAAcC,iDAA2B,KAAK;EAC9C,QAAQC,oCAAqB,KAAK;EAClC,kBAAkBC,yDAA+B,KAAK;EACtD,YAAYC,4CAAyB,KAAK;EAC1C,mBAAmBC,2DAAgC,KAAK;EACxD,aAAaC,8CAA0B,KAAK;EAC5C,QAAQ;EACR,KAAK;EACL,OAAO;EACP,cAAc,OAAO;EACrB,cAAcC,iDAA2B,KAAK;EAC9C,MAAM;EACN,SAASC,sCAAsB,KAAK;EACpC,KAAK;EACL,QAAQC,oCAAqB,KAAK;EAClC,SAASC,sCAAsB,KAAK;EACpC,OAAOC,kCAAoB;GAAE;GAAQ;GAAa,CAAC;EACpD"}
|
package/dist/index.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { Client, ResolvedRequestOptions } from './generated/shared/client';\nimport type { Middleware } from './generated/shared/client/utils.gen';\nimport { createClient, createConfig } from './generated/shared/client';\nimport { getManagementBaseUrl } from '@storyblok/region-helper';\nimport { ClientError } from './error';\nimport { createThrottleManager } from './utils/rate-limit';\nimport { createAssetFoldersResource } from './resources/asset-folders';\nimport { createAssetsResource } from './resources/assets';\nimport { createComponentFoldersResource } from './resources/component-folders';\nimport { createComponentsResource } from './resources/components';\nimport { createDatasourceEntriesResource } from './resources/datasource-entries';\nimport { createDatasourcesResource } from './resources/datasources';\nimport { createInternalTagsResource } from './resources/internal-tags';\nimport { createPresetsResource } from './resources/presets';\nimport { createSpacesResource } from './resources/spaces';\nimport { createStoriesResource } from './resources/stories';\nimport { createUsersResource } from './resources/users';\nimport type {\n ApiResponse,\n HttpRequestOptions,\n ManagementApiClientConfig,\n MapiResourceDeps,\n} from './types';\n\nexport { ClientError } from './error';\n\nfunction getAuthorizationHeader(config: ManagementApiClientConfig): string | undefined {\n if (config.personalAccessToken) {\n return config.personalAccessToken;\n }\n if (config.oauthToken) {\n return `Bearer ${config.oauthToken.replace(/^Bearer\\s+/i, '')}`;\n }\n return undefined;\n}\n\nexport const createManagementApiClient = (config: ManagementApiClientConfig) => {\n const {\n spaceId,\n region = 'eu',\n baseUrl,\n headers = {},\n throwOnError = false,\n retry = {\n limit: 12,\n backoffLimit: 20_000,\n methods: ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace'],\n statusCodes: [429],\n },\n timeout = 30_000,\n rateLimit,\n } = config;\n\n const throttleManager = createThrottleManager(rateLimit ?? {});\n const authHeader = getAuthorizationHeader(config);\n\n const client: Client = createClient(\n createConfig({\n baseUrl: baseUrl || getManagementBaseUrl(region),\n headers: {\n ...(authHeader ? { Authorization: authHeader } : {}),\n ...headers,\n },\n throwOnError,\n kyOptions: {\n throwHttpErrors: true,\n timeout,\n retry,\n },\n }),\n );\n\n client.interceptors.error.use(\n (error: unknown, response: Response) =>\n new ClientError(response?.statusText || 'API request failed', {\n status: response?.status ?? 0,\n statusText: response?.statusText ?? '',\n data: error,\n }),\n );\n\n /**\n * Wraps an SDK call with throttling and response adaptation.\n * The throttle slot is held for the entire duration of the request.\n * When throwOnError is true, errors throw and data is guaranteed non-null.\n */\n function wrapRequest<TData, ThrowOnError extends boolean = false>(\n fn: () => Promise<unknown>,\n _throwOnError?: ThrowOnError,\n ): Promise<ApiResponse<TData, ThrowOnError>> {\n return throttleManager.execute(async () => {\n const result = await fn() as ApiResponse<TData, ThrowOnError>;\n throttleManager.adaptToResponse((result as { response: Response }).response);\n return result;\n }) as Promise<ApiResponse<TData, ThrowOnError>>;\n }\n\n const deps: MapiResourceDeps = { client, spaceId, wrapRequest };\n\n /**\n * Escape hatch: send a GET request to any MAPI endpoint not yet wrapped\n * in a dedicated resource method.\n */\n const httpGet = <TData = unknown>(\n path: string,\n options: HttpRequestOptions = {},\n ): Promise<ApiResponse<TData>> => {\n const { fetchOptions, ...rest } = options;\n return wrapRequest<TData>(() =>\n client.get({ url: path, ...rest, ...(fetchOptions ? { kyOptions: { ...client.getConfig().kyOptions, ...fetchOptions } } : {}) }),\n );\n };\n\n /**\n * Escape hatch: send a POST request to any MAPI endpoint not yet wrapped\n * in a dedicated resource method.\n */\n const httpPost = <TData = unknown>(\n path: string,\n options: HttpRequestOptions = {},\n ): Promise<ApiResponse<TData>> => {\n const { fetchOptions, ...rest } = options;\n return wrapRequest<TData>(() =>\n client.post({ url: path, ...rest, ...(fetchOptions ? { kyOptions: { ...client.getConfig().kyOptions, ...fetchOptions } } : {}) }),\n );\n };\n\n /**\n * Escape hatch: send a PUT request to any MAPI endpoint not yet wrapped\n * in a dedicated resource method.\n */\n const httpPut = <TData = unknown>(\n path: string,\n options: HttpRequestOptions = {},\n ): Promise<ApiResponse<TData>> => {\n const { fetchOptions, ...rest } = options;\n return wrapRequest<TData>(() =>\n client.put({ url: path, ...rest, ...(fetchOptions ? { kyOptions: { ...client.getConfig().kyOptions, ...fetchOptions } } : {}) }),\n );\n };\n\n /**\n * Escape hatch: send a PATCH request to any MAPI endpoint not yet wrapped\n * in a dedicated resource method.\n */\n const httpPatch = <TData = unknown>(\n path: string,\n options: HttpRequestOptions = {},\n ): Promise<ApiResponse<TData>> => {\n const { fetchOptions, ...rest } = options;\n return wrapRequest<TData>(() =>\n client.patch({ url: path, ...rest, ...(fetchOptions ? { kyOptions: { ...client.getConfig().kyOptions, ...fetchOptions } } : {}) }),\n );\n };\n\n /**\n * Escape hatch: send a DELETE request to any MAPI endpoint not yet wrapped\n * in a dedicated resource method.\n */\n const httpDelete = <TData = unknown>(\n path: string,\n options: HttpRequestOptions = {},\n ): Promise<ApiResponse<TData>> => {\n const { fetchOptions, ...rest } = options;\n return wrapRequest<TData>(() =>\n client.delete({ url: path, ...rest, ...(fetchOptions ? { kyOptions: { ...client.getConfig().kyOptions, ...fetchOptions } } : {}) }),\n );\n };\n\n return {\n assetFolders: createAssetFoldersResource(deps),\n assets: createAssetsResource(deps),\n componentFolders: createComponentFoldersResource(deps),\n components: createComponentsResource(deps),\n datasourceEntries: createDatasourceEntriesResource(deps),\n datasources: createDatasourcesResource(deps),\n delete: httpDelete,\n get: httpGet,\n patch: httpPatch,\n interceptors: client.interceptors as Middleware<Request, Response, unknown, ResolvedRequestOptions>,\n internalTags: createInternalTagsResource(deps),\n post: httpPost,\n presets: createPresetsResource(deps),\n put: httpPut,\n spaces: createSpacesResource(deps),\n stories: createStoriesResource(deps),\n users: createUsersResource({ client, wrapRequest }),\n };\n};\n\nexport type ManagementApiClient = ReturnType<typeof createManagementApiClient>;\n\nexport type {\n ApiResponse,\n Asset,\n AssetCreate,\n AssetField,\n AssetFolder,\n AssetFolderCreate,\n AssetFolderUpdate,\n AssetListQuery,\n AssetSignRequest,\n AssetUpdate,\n AssetUpdateRequest,\n AssetUploadRequest,\n Component,\n ComponentCreate,\n ComponentFolder,\n ComponentSchemaField,\n ComponentUpdate,\n Datasource,\n DatasourceCreate,\n DatasourceEntry,\n DatasourceEntryCreate,\n DatasourceEntryUpdate,\n DatasourceUpdate,\n FetchOptions,\n HttpRequestOptions,\n InternalTag,\n ManagementApiClientConfig,\n MapiResourceDeps,\n MultilinkField,\n PluginField,\n Preset,\n RateLimitConfig,\n RequestConfigOverrides,\n RichtextField,\n SignedResponseObject,\n Space,\n SpaceCreate,\n SpaceUpdate,\n Story,\n StoryAlternate,\n StoryContent,\n StoryCreate,\n StoryListQuery,\n StoryLocalizedPath,\n StoryTranslatedSlug,\n StoryUpdate,\n TableField,\n User,\n} from './types';\nexport { normalizeAssetUrl } from './utils/normalize-asset-url';\n"],"mappings":"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"}
|