@storyblok/management-api-client 1.0.0-alpha.2 → 1.0.0-alpha.4

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.
Files changed (54) hide show
  1. package/dist/client.cjs +6 -0
  2. package/dist/client.cjs.map +1 -1
  3. package/dist/client.d.cts +133 -7
  4. package/dist/client.d.mts +133 -7
  5. package/dist/client.mjs +6 -0
  6. package/dist/client.mjs.map +1 -1
  7. package/dist/generated/mapi/_internal.gen.d.cts +1 -171
  8. package/dist/generated/mapi/_internal.gen.d.mts +1 -171
  9. package/dist/generated/mapi/types-aliased.gen.d.cts +171 -1
  10. package/dist/generated/mapi/types-aliased.gen.d.mts +171 -1
  11. package/dist/generated/types/block.d.cts +27 -8
  12. package/dist/generated/types/block.d.mts +27 -8
  13. package/dist/generated/types/field.d.cts +45 -17
  14. package/dist/generated/types/field.d.mts +45 -17
  15. package/dist/generated/types/mapi-story.d.cts +6 -6
  16. package/dist/generated/types/mapi-story.d.mts +6 -6
  17. package/dist/index.d.cts +6 -4
  18. package/dist/index.d.mts +6 -4
  19. package/dist/resources/assets.cjs +1 -0
  20. package/dist/resources/assets.cjs.map +1 -1
  21. package/dist/resources/assets.mjs +1 -1
  22. package/dist/resources/assets.mjs.map +1 -1
  23. package/dist/resources/components.cjs.map +1 -1
  24. package/dist/resources/components.d.cts +17 -17
  25. package/dist/resources/components.d.mts +17 -17
  26. package/dist/resources/components.mjs.map +1 -1
  27. package/dist/resources/shared-asset-folders.cjs +81 -0
  28. package/dist/resources/shared-asset-folders.cjs.map +1 -0
  29. package/dist/resources/shared-asset-folders.d.cts +41 -0
  30. package/dist/resources/shared-asset-folders.d.mts +41 -0
  31. package/dist/resources/shared-asset-folders.mjs +81 -0
  32. package/dist/resources/shared-asset-folders.mjs.map +1 -0
  33. package/dist/resources/shared-assets.cjs +197 -0
  34. package/dist/resources/shared-assets.cjs.map +1 -0
  35. package/dist/resources/shared-assets.d.cts +29 -0
  36. package/dist/resources/shared-assets.d.mts +28 -0
  37. package/dist/resources/shared-assets.mjs +197 -0
  38. package/dist/resources/shared-assets.mjs.map +1 -0
  39. package/dist/resources/shared-internal-tags.cjs +73 -0
  40. package/dist/resources/shared-internal-tags.cjs.map +1 -0
  41. package/dist/resources/shared-internal-tags.d.cts +32 -0
  42. package/dist/resources/shared-internal-tags.d.mts +32 -0
  43. package/dist/resources/shared-internal-tags.mjs +73 -0
  44. package/dist/resources/shared-internal-tags.mjs.map +1 -0
  45. package/dist/resources/stories.cjs.map +1 -1
  46. package/dist/resources/stories.d.cts +26 -26
  47. package/dist/resources/stories.d.mts +26 -26
  48. package/dist/resources/stories.mjs.map +1 -1
  49. package/package.json +3 -3
  50. package/playground/integration-tests/test/specs/mapi-round-trip.spec.e2e.ts +30 -40
  51. package/playground/integration-tests/test/types/components.test-d.ts +7 -46
  52. package/playground/integration-tests/test/types/resources.test-d.ts +0 -152
  53. package/playground/integration-tests/test/types/stories.test-d.ts +47 -43
  54. package/test/GUIDE.md +2 -2
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@storyblok/management-api-client",
3
3
  "type": "module",
4
- "version": "1.0.0-alpha.2",
4
+ "version": "1.0.0-alpha.4",
5
5
  "private": false,
6
6
  "description": "Storyblok Management API Client",
7
7
  "author": "",
@@ -32,7 +32,7 @@
32
32
  "dependencies": {
33
33
  "async-sema": "^3.1.1",
34
34
  "ky": "^1.14.3",
35
- "@storyblok/region-helper": "1.4.1"
35
+ "@storyblok/region-helper": "1.5.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^24.1.0",
@@ -42,7 +42,7 @@
42
42
  "tsdown": "^0.20.3",
43
43
  "tsx": "^4.20.3",
44
44
  "vitest": "^4.1.3",
45
- "@storyblok/eslint-config": "0.5.1",
45
+ "@storyblok/eslint-config": "0.6.0",
46
46
  "@storyblok/openapi-codegen": "0.0.1"
47
47
  },
48
48
  "nx": {
@@ -18,16 +18,10 @@
18
18
 
19
19
  import { afterAll, beforeAll, describe, expect, it } from 'vitest';
20
20
  import { createManagementApiClient } from '@storyblok/management-api-client';
21
+ import type { StoryCreate, StoryUpdate } from '@storyblok/management-api-client';
21
22
  import {
22
- createStoryHelpers,
23
23
  defineBlock,
24
- defineBlockCreate,
25
- defineBlockFolderCreate,
26
- defineDatasourceCreate,
27
- defineDatasourceEntryCreate,
28
24
  defineField,
29
- defineInternalTagCreate,
30
- definePresetCreate,
31
25
  } from '@storyblok/schema';
32
26
 
33
27
  const token = process.env.STORYBLOK_TOKEN!;
@@ -43,7 +37,7 @@ const STORY_SLUG = `${STORY_SLUG_PREFIX}test-page`;
43
37
 
44
38
  const teaserComponent = defineBlock({
45
39
  name: `${PREFIX}teaser`,
46
- schema: [
40
+ fields: [
47
41
  defineField('title', { type: 'text', required: true }),
48
42
  defineField('image', { type: 'asset' }),
49
43
  ],
@@ -51,21 +45,21 @@ const teaserComponent = defineBlock({
51
45
  // Level-2 container: holds teasers in its `items` bloks field (level 3)
52
46
  const sectionComponent = defineBlock({
53
47
  name: `${PREFIX}section`,
54
- schema: [
48
+ fields: [
55
49
  defineField('title', { type: 'text' }),
56
- defineField('items', { type: 'bloks', component_whitelist: [teaserComponent.name], required: true }),
50
+ defineField('items', { type: 'bloks', allow: [teaserComponent.name], required: true }),
57
51
  ],
58
52
  });
59
53
  const pageComponent = defineBlock({
60
54
  name: `${PREFIX}page`,
61
55
  is_root: true,
62
- schema: [
56
+ fields: [
63
57
  defineField('headline', { type: 'text', required: true }),
64
58
  defineField('rating', { type: 'number' }),
65
59
  defineField('is_featured', { type: 'boolean' }),
66
60
  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 }),
61
+ defineField('body', { type: 'bloks', allow: [teaserComponent.name, sectionComponent.name], required: true }),
62
+ defineField('category', { type: 'option', source: 'internal', datasource: DATASOURCE_SLUG }),
69
63
  defineField('any_blocks', { type: 'bloks', required: true }),
70
64
  ],
71
65
  });
@@ -74,7 +68,7 @@ interface StoryblokTypes {
74
68
  components: typeof pageComponent | typeof teaserComponent | typeof sectionComponent;
75
69
  }
76
70
 
77
- const { defineStoryCreate, defineStoryUpdate } = createStoryHelpers().withTypes<StoryblokTypes>();
71
+ type Blocks = StoryblokTypes['components'];
78
72
 
79
73
  const client = createManagementApiClient({
80
74
  personalAccessToken: token,
@@ -155,53 +149,50 @@ describe('schema + mapi-client MAPI round-trip', () => {
155
149
 
156
150
  // 1. Datasource + entries first — the page component schema references the datasource slug,
157
151
  // and the MAPI validates that the datasource exists at component creation time.
158
- const datasourcePayload = defineDatasourceCreate({
152
+ // Create payloads are plain MAPI wire objects validated by the typed client.
153
+ const dsRes = await client.datasources.create({ body: { datasource: {
159
154
  name: DATASOURCE_NAME,
160
155
  slug: DATASOURCE_SLUG,
161
- });
162
- const dsRes = await client.datasources.create({ body: { datasource: datasourcePayload } });
156
+ } } });
163
157
  datasourceId = dsRes.data!.datasource!.id!;
164
158
 
165
159
  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 }),
160
+ { name: 'Technology', value: 'tech', datasource_id: datasourceId },
161
+ { name: 'Design', value: 'design', datasource_id: datasourceId },
162
+ { name: 'Business', value: 'business', datasource_id: datasourceId },
169
163
  ]) {
170
164
  await client.datasourceEntries.create({ body: { datasource_entry: entry } });
171
165
  }
172
166
 
173
167
  // 2. Component folder
174
- const folderPayload = defineBlockFolderCreate({ name: `${PREFIX}folder` });
175
- const folderRes = await client.componentFolders.create({ body: { component_group: folderPayload } });
168
+ const folderRes = await client.componentFolders.create({ body: { component_group: { name: `${PREFIX}folder` } } });
176
169
  componentFolderId = folderRes.data!.component_group!.id!;
177
170
  const folderUuid = folderRes.data!.component_group!.uuid;
178
171
 
179
172
  // 3. Teaser component (innermost — whitelisted by section)
180
- const teaserPayload = defineBlockCreate({
173
+ const teaserRes = await client.components.create({ body: { component: {
181
174
  name: teaserComponent.name,
182
175
  schema: {
183
176
  title: { type: 'text', required: true, pos: 0 },
184
177
  image: { type: 'asset', pos: 1 },
185
178
  },
186
179
  component_group_uuid: folderUuid,
187
- });
188
- const teaserRes = await client.components.create({ body: { component: teaserPayload } });
180
+ } } });
189
181
  teaserComponentId = teaserRes.data!.component!.id!;
190
182
 
191
183
  // 4. Section component (level 2 — whitelists teaser, whitelisted by page)
192
- const sectionPayload = defineBlockCreate({
184
+ const sectionRes = await client.components.create({ body: { component: {
193
185
  name: sectionComponent.name,
194
186
  schema: {
195
187
  title: { type: 'text', pos: 0 },
196
188
  items: { type: 'bloks', component_whitelist: [teaserComponent.name], pos: 1 },
197
189
  },
198
190
  component_group_uuid: folderUuid,
199
- });
200
- const sectionRes = await client.components.create({ body: { component: sectionPayload } });
191
+ } } });
201
192
  sectionComponentId = sectionRes.data!.component!.id!;
202
193
 
203
194
  // 5. Page component (level 1 — whitelists both teaser and section in body)
204
- const pagePayload = defineBlockCreate({
195
+ const pageRes = await client.components.create({ body: { component: {
205
196
  name: pageComponent.name,
206
197
  schema: {
207
198
  headline: { type: 'text', required: true, pos: 0 },
@@ -214,30 +205,28 @@ describe('schema + mapi-client MAPI round-trip', () => {
214
205
  },
215
206
  component_group_uuid: folderUuid,
216
207
  is_root: true,
217
- });
218
- const pageRes = await client.components.create({ body: { component: pagePayload } });
208
+ } } });
219
209
  pageComponentId = pageRes.data!.component!.id!;
220
210
 
221
211
  // 5. Internal tag
222
- const tagPayload = defineInternalTagCreate({ name: `${PREFIX}tag`, object_type: 'component' });
223
- const tagRes = await client.internalTags.create({ body: { internal_tag: tagPayload } });
212
+ const tagRes = await client.internalTags.create({ body: { internal_tag: { name: `${PREFIX}tag`, object_type: 'component' } } });
224
213
  internalTagId = tagRes.data!.internal_tag!.id!;
225
214
 
226
215
  // 6. Preset for page component
227
- const presetPayload = definePresetCreate({
216
+ const presetRes = await client.presets.create({ body: { preset: {
228
217
  name: `${PREFIX}default_page`,
229
218
  component_id: pageComponentId,
230
219
  preset: { headline: 'Default Headline', rating: 0, is_featured: false },
231
220
  description: `Default preset for ${pageComponent.name}`,
232
- });
233
- const presetRes = await client.presets.create({ body: { preset: presetPayload } });
221
+ } } });
234
222
  presetId = presetRes.data!.preset!.id!;
235
223
 
236
224
  // 8. Story: body[0]=teaser (level 2), body[1]=section{items:[teaser]} (levels 2+3)
237
- const storyPayload = defineStoryCreate(pageComponent, {
225
+ const storyPayload: StoryCreate<Blocks> = {
238
226
  name: STORY_NAME,
239
227
  slug: STORY_SLUG,
240
228
  content: {
229
+ component: pageComponent.name,
241
230
  headline: 'Hello from e2e',
242
231
  rating: 42,
243
232
  is_featured: true,
@@ -269,7 +258,7 @@ describe('schema + mapi-client MAPI round-trip', () => {
269
258
  },
270
259
  ],
271
260
  },
272
- });
261
+ };
273
262
  const storyRes = await client.stories.create({ body: { story: storyPayload } });
274
263
  storyId = storyRes.data!.story!.id!;
275
264
  });
@@ -485,10 +474,11 @@ describe('schema + mapi-client MAPI round-trip', () => {
485
474
  });
486
475
 
487
476
  it('should round-trip story update correctly', async () => {
488
- const updatedPayload = defineStoryUpdate(pageComponent, {
477
+ const updatedPayload: StoryUpdate<Blocks> = {
489
478
  name: `${STORY_NAME} (Updated)`,
490
479
  slug: STORY_SLUG,
491
480
  content: {
481
+ component: pageComponent.name,
492
482
  headline: 'Updated headline',
493
483
  rating: 100,
494
484
  is_featured: false,
@@ -500,7 +490,7 @@ describe('schema + mapi-client MAPI round-trip', () => {
500
490
  category: 'design',
501
491
  any_blocks: [],
502
492
  },
503
- });
493
+ };
504
494
 
505
495
  await client.stories.update(storyId, { body: { story: updatedPayload } });
506
496
 
@@ -1,11 +1,11 @@
1
- import { defineBlock, defineBlockCreate, defineBlockUpdate, defineField } from '@storyblok/schema';
1
+ import { defineBlock, defineField } from '@storyblok/schema';
2
2
  import { type Component as ComponentMapi, createManagementApiClient } from '@storyblok/management-api-client';
3
3
  import { describe, expectTypeOf, it } from 'vitest';
4
4
 
5
5
  // Nestable block — not a root story type
6
6
  const teaserComponent = defineBlock({
7
7
  name: 'teaser',
8
- schema: [
8
+ fields: [
9
9
  defineField('text', { type: 'text' }),
10
10
  defineField('image', { type: 'asset' }),
11
11
  ],
@@ -16,49 +16,23 @@ const _pageComponent = defineBlock({
16
16
  name: 'page',
17
17
  is_root: true,
18
18
  is_nestable: false,
19
- schema: [
19
+ fields: [
20
20
  defineField('headline', { type: 'text', required: true }),
21
21
  defineField('body', { type: 'richtext' }),
22
- defineField('teasers', { type: 'bloks', component_whitelist: [teaserComponent.name] }),
22
+ defineField('teasers', { type: 'bloks', allow: [teaserComponent.name] }),
23
23
  ],
24
24
  });
25
25
 
26
26
  const CLIENT_CONFIG = { personalAccessToken: 'test-token', spaceId: 12345 };
27
27
 
28
- describe('components.create body type compatibility', () => {
29
- it('should produce a defineBlockCreate result assignable to components.create body', () => {
30
- const createPayload = defineBlockCreate({
31
- name: 'article',
32
- schema: {
33
- title: { type: 'text', pos: 1 },
34
- },
35
- });
36
-
37
- type CreateBody = Parameters<ReturnType<typeof createManagementApiClient>['components']['create']>[0]['body'];
38
- type ComponentCreateInput = NonNullable<CreateBody['component']>;
39
-
40
- expectTypeOf(createPayload).toExtend<ComponentCreateInput>();
41
- });
42
-
43
- it('should produce a defineBlockUpdate result assignable to components.update body', () => {
44
- const updatePayload = defineBlockUpdate({
45
- display_name: 'Article',
46
- });
47
-
48
- type UpdateBody = Parameters<ReturnType<typeof createManagementApiClient>['components']['update']>[1]['body'];
49
- type ComponentUpdateInput = NonNullable<UpdateBody['component']>;
50
-
51
- expectTypeOf(updatePayload).toExtend<ComponentUpdateInput>();
52
- });
53
- });
54
-
55
28
  describe('components.get response shape', () => {
56
- it('should return a Component from components.get matching the wire shape', async () => {
29
+ it('should return the wire Component (a `schema` record) from components.get', async () => {
57
30
  const client = createManagementApiClient(CLIENT_CONFIG);
58
31
  const result = await client.components.get(123);
59
32
 
60
33
  if (result.data?.component) {
61
- // components.get() returns the wrapper Component (= Block), matching the wire shape
34
+ // components.get() returns the wire-shaped Component definition (a `schema`
35
+ // record), not the DSL `fields` block.
62
36
  expectTypeOf(result.data.component.id).toEqualTypeOf<ComponentMapi['id']>();
63
37
  expectTypeOf(result.data.component.name).toEqualTypeOf<ComponentMapi['name']>();
64
38
  expectTypeOf(result.data.component).toHaveProperty('schema');
@@ -66,19 +40,6 @@ describe('components.get response shape', () => {
66
40
  });
67
41
  });
68
42
 
69
- describe('components.create body type rejection', () => {
70
- it('should reject a component create payload with wrong schema field type', () => {
71
- const createPayload = defineBlockCreate({
72
- name: 'article',
73
- schema: {
74
- // @ts-expect-error: schema value must be a field definition, not a string
75
- title: 'invalid',
76
- },
77
- });
78
- void createPayload;
79
- });
80
- });
81
-
82
43
  describe('defineBlock result used in .withTypes() interface', () => {
83
44
  interface StoryblokTypes {
84
45
  components: typeof _pageComponent | typeof teaserComponent;
@@ -1,44 +1,9 @@
1
- import {
2
- defineAssetFolderCreate,
3
- defineAssetFolderUpdate,
4
- defineBlockFolderCreate,
5
- defineBlockFolderUpdate,
6
- defineDatasourceCreate,
7
- defineDatasourceEntryCreate,
8
- defineDatasourceEntryUpdate,
9
- defineDatasourceUpdate,
10
- defineInternalTagCreate,
11
- defineInternalTagUpdate,
12
- definePresetCreate,
13
- definePresetUpdate,
14
- defineSpaceCreate,
15
- defineSpaceUpdate,
16
- defineUserUpdate,
17
- } from '@storyblok/schema';
18
1
  import { createManagementApiClient } from '@storyblok/management-api-client';
19
2
  import { describe, expectTypeOf, it } from 'vitest';
20
3
 
21
4
  const CLIENT_CONFIG = { personalAccessToken: 'test-token', spaceId: 12345 };
22
5
 
23
6
  describe('datasources type tests', () => {
24
- it('should produce a defineDatasourceCreate result assignable to datasources.create body', () => {
25
- const payload = defineDatasourceCreate({ name: 'Categories', slug: 'categories' });
26
-
27
- type CreateBody = Parameters<ReturnType<typeof createManagementApiClient>['datasources']['create']>[0]['body'];
28
- type InnerType = NonNullable<CreateBody['datasource']>;
29
-
30
- expectTypeOf(payload).toExtend<InnerType>();
31
- });
32
-
33
- it('should produce a defineDatasourceUpdate result assignable to datasources.update body', () => {
34
- const payload = defineDatasourceUpdate({ name: 'Updated Categories' });
35
-
36
- type UpdateBody = Parameters<ReturnType<typeof createManagementApiClient>['datasources']['update']>[1]['body'];
37
- type InnerType = NonNullable<UpdateBody['datasource']>;
38
-
39
- expectTypeOf(payload).toExtend<InnerType>();
40
- });
41
-
42
7
  it('should return datasource in response from create', async () => {
43
8
  const client = createManagementApiClient(CLIENT_CONFIG);
44
9
  const result = await client.datasources.create({
@@ -67,24 +32,6 @@ describe('datasources type tests', () => {
67
32
  });
68
33
 
69
34
  describe('datasource entries type tests', () => {
70
- it('should produce a defineDatasourceEntryCreate result assignable to datasourceEntries.create body', () => {
71
- const payload = defineDatasourceEntryCreate({ name: 'red', value: '#ff0000', datasource_id: 42 });
72
-
73
- type CreateBody = Parameters<ReturnType<typeof createManagementApiClient>['datasourceEntries']['create']>[0]['body'];
74
- type InnerType = NonNullable<CreateBody['datasource_entry']>;
75
-
76
- expectTypeOf(payload).toExtend<InnerType>();
77
- });
78
-
79
- it('should produce a defineDatasourceEntryUpdate result assignable to datasourceEntries.update body', () => {
80
- const payload = defineDatasourceEntryUpdate({ value: '#00ff00' });
81
-
82
- type UpdateBody = Parameters<ReturnType<typeof createManagementApiClient>['datasourceEntries']['update']>[1]['body'];
83
- type InnerType = NonNullable<UpdateBody['datasource_entry']>;
84
-
85
- expectTypeOf(payload).toExtend<InnerType>();
86
- });
87
-
88
35
  it('should return datasource_entry in response from create', async () => {
89
36
  const client = createManagementApiClient(CLIENT_CONFIG);
90
37
  const result = await client.datasourceEntries.create({
@@ -113,24 +60,6 @@ describe('datasource entries type tests', () => {
113
60
  });
114
61
 
115
62
  describe('asset folders type tests', () => {
116
- it('should produce a defineAssetFolderCreate result assignable to assetFolders.create body', () => {
117
- const payload = defineAssetFolderCreate({ name: 'Images' });
118
-
119
- type CreateBody = Parameters<ReturnType<typeof createManagementApiClient>['assetFolders']['create']>[0]['body'];
120
- type InnerType = NonNullable<CreateBody['asset_folder']>;
121
-
122
- expectTypeOf(payload).toExtend<InnerType>();
123
- });
124
-
125
- it('should produce a defineAssetFolderUpdate result assignable to assetFolders.update body', () => {
126
- const payload = defineAssetFolderUpdate({ name: 'Updated Images' });
127
-
128
- type UpdateBody = Parameters<ReturnType<typeof createManagementApiClient>['assetFolders']['update']>[1]['body'];
129
- type InnerType = NonNullable<UpdateBody['asset_folder']>;
130
-
131
- expectTypeOf(payload).toExtend<InnerType>();
132
- });
133
-
134
63
  it('should return asset_folder in response from create', async () => {
135
64
  const client = createManagementApiClient(CLIENT_CONFIG);
136
65
  const result = await client.assetFolders.create({
@@ -159,24 +88,6 @@ describe('asset folders type tests', () => {
159
88
  });
160
89
 
161
90
  describe('component folders type tests', () => {
162
- it('should produce a defineBlockFolderCreate result assignable to componentFolders.create body', () => {
163
- const payload = defineBlockFolderCreate({ name: 'Layout' });
164
-
165
- type CreateBody = Parameters<ReturnType<typeof createManagementApiClient>['componentFolders']['create']>[0]['body'];
166
- type InnerType = NonNullable<CreateBody['component_group']>;
167
-
168
- expectTypeOf(payload).toExtend<InnerType>();
169
- });
170
-
171
- it('should produce a defineBlockFolderUpdate result assignable to componentFolders.update body', () => {
172
- const payload = defineBlockFolderUpdate({ name: 'Updated Layout' });
173
-
174
- type UpdateBody = Parameters<ReturnType<typeof createManagementApiClient>['componentFolders']['update']>[1]['body'];
175
- type InnerType = NonNullable<UpdateBody['component_group']>;
176
-
177
- expectTypeOf(payload).toExtend<InnerType>();
178
- });
179
-
180
91
  it('should return component_group in response from create', async () => {
181
92
  const client = createManagementApiClient(CLIENT_CONFIG);
182
93
  const result = await client.componentFolders.create({
@@ -205,24 +116,6 @@ describe('component folders type tests', () => {
205
116
  });
206
117
 
207
118
  describe('internal tags type tests', () => {
208
- it('should produce a defineInternalTagCreate result assignable to internalTags.create body', () => {
209
- const payload = defineInternalTagCreate({ name: 'hero', object_type: 'asset' });
210
-
211
- type CreateBody = Parameters<ReturnType<typeof createManagementApiClient>['internalTags']['create']>[0]['body'];
212
- type InnerType = NonNullable<CreateBody['internal_tag']>;
213
-
214
- expectTypeOf(payload).toExtend<InnerType>();
215
- });
216
-
217
- it('should produce a defineInternalTagUpdate result assignable to internalTags.update body', () => {
218
- const payload = defineInternalTagUpdate({ name: 'hero-image' });
219
-
220
- type UpdateBody = Parameters<ReturnType<typeof createManagementApiClient>['internalTags']['update']>[1]['body'];
221
- type InnerType = NonNullable<UpdateBody['internal_tag']>;
222
-
223
- expectTypeOf(payload).toExtend<InnerType>();
224
- });
225
-
226
119
  it('should return internal_tag in response from create', async () => {
227
120
  const client = createManagementApiClient(CLIENT_CONFIG);
228
121
  const result = await client.internalTags.create({
@@ -243,24 +136,6 @@ describe('internal tags type tests', () => {
243
136
  });
244
137
 
245
138
  describe('presets type tests', () => {
246
- it('should produce a definePresetCreate result assignable to presets.create body', () => {
247
- const payload = definePresetCreate({ name: 'Hero Dark', component_id: 42 });
248
-
249
- type CreateBody = Parameters<ReturnType<typeof createManagementApiClient>['presets']['create']>[0]['body'];
250
- type InnerType = NonNullable<CreateBody['preset']>;
251
-
252
- expectTypeOf(payload).toExtend<InnerType>();
253
- });
254
-
255
- it('should produce a definePresetUpdate result assignable to presets.update body', () => {
256
- const payload = definePresetUpdate({ name: 'Hero Light' });
257
-
258
- type UpdateBody = Parameters<ReturnType<typeof createManagementApiClient>['presets']['update']>[1]['body'];
259
- type InnerType = NonNullable<UpdateBody['preset']>;
260
-
261
- expectTypeOf(payload).toExtend<InnerType>();
262
- });
263
-
264
139
  it('should return preset in response from create', async () => {
265
140
  const client = createManagementApiClient(CLIENT_CONFIG);
266
141
  const result = await client.presets.create({
@@ -289,24 +164,6 @@ describe('presets type tests', () => {
289
164
  });
290
165
 
291
166
  describe('spaces type tests', () => {
292
- it('should produce a defineSpaceCreate result assignable to spaces.create body', () => {
293
- const payload = defineSpaceCreate({ name: 'My New Space' });
294
-
295
- type CreateBody = Parameters<ReturnType<typeof createManagementApiClient>['spaces']['create']>[0]['body'];
296
- type InnerType = CreateBody['space'];
297
-
298
- expectTypeOf(payload).toExtend<InnerType>();
299
- });
300
-
301
- it('should produce a defineSpaceUpdate result assignable to spaces.update body', () => {
302
- const payload = defineSpaceUpdate({ name: 'Updated Space Name' });
303
-
304
- type UpdateBody = Parameters<ReturnType<typeof createManagementApiClient>['spaces']['update']>[0]['body'];
305
- type InnerType = UpdateBody['space'];
306
-
307
- expectTypeOf(payload).toExtend<InnerType>();
308
- });
309
-
310
167
  it('should return space in response from create', async () => {
311
168
  const client = createManagementApiClient(CLIENT_CONFIG);
312
169
  const result = await client.spaces.create({
@@ -335,15 +192,6 @@ describe('spaces type tests', () => {
335
192
  });
336
193
 
337
194
  describe('users type tests', () => {
338
- it('should produce a defineUserUpdate result assignable to users.updateMe body', () => {
339
- const payload = defineUserUpdate({ firstname: 'Jane', lastname: 'Doe' });
340
-
341
- type UpdateBody = Parameters<ReturnType<typeof createManagementApiClient>['users']['updateMe']>[0]['body'];
342
- type InnerType = UpdateBody['user'];
343
-
344
- expectTypeOf(payload).toExtend<InnerType>();
345
- });
346
-
347
195
  it('should return user in response from updateMe', async () => {
348
196
  const client = createManagementApiClient(CLIENT_CONFIG);
349
197
  const result = await client.users.updateMe({
@@ -1,11 +1,12 @@
1
- import { defineBlock, defineField, defineStoryCreate, defineStoryUpdate } from '@storyblok/schema';
2
- import { createManagementApiClient, type Story as StoryMapi } from '@storyblok/management-api-client';
1
+ import { defineBlock, defineField, defineSchema, type Schema as InferSchema } from '@storyblok/schema';
2
+ import { storyblokColorField } from '@storyblok/schema/field-plugins';
3
+ import { createManagementApiClient, type PluginFieldValue, type Story as StoryMapi } from '@storyblok/management-api-client';
3
4
  import { describe, expectTypeOf, it } from 'vitest';
4
5
 
5
6
  // Nestable block — not a root story type
6
7
  const teaserComponent = defineBlock({
7
8
  name: 'teaser',
8
- schema: [
9
+ fields: [
9
10
  defineField('text', { type: 'text' }),
10
11
  defineField('image', { type: 'asset' }),
11
12
  ],
@@ -18,7 +19,7 @@ const teaserComponent = defineBlock({
18
19
  const heroComponent = defineBlock({
19
20
  name: 'hero',
20
21
  is_root: true,
21
- schema: [
22
+ fields: [
22
23
  defineField('title', { type: 'text' }),
23
24
  defineField('count', { type: 'number' }),
24
25
  // bloks field without a whitelist — resolves to nestable components only
@@ -34,12 +35,12 @@ const _pageComponent = defineBlock({
34
35
  name: 'page',
35
36
  is_root: true,
36
37
  is_nestable: false,
37
- schema: [
38
+ fields: [
38
39
  defineField('headline', { type: 'text' }),
39
40
  defineField('body', { type: 'richtext' }),
40
- defineField('teasers', { type: 'bloks', component_whitelist: [teaserComponent.name] }),
41
- defineField('hero', { type: 'bloks', component_whitelist: [heroComponent.name] }),
42
- defineField('blocks', { type: 'bloks', component_whitelist: [heroComponent.name, teaserComponent.name] }),
41
+ defineField('teasers', { type: 'bloks', allow: [teaserComponent.name] }),
42
+ defineField('hero', { type: 'bloks', allow: [heroComponent.name] }),
43
+ defineField('blocks', { type: 'bloks', allow: [heroComponent.name, teaserComponent.name] }),
43
44
  ],
44
45
  id: 0,
45
46
  created_at: '',
@@ -237,36 +238,7 @@ describe('createManagementApiClient with .withTypes()', () => {
237
238
  });
238
239
  });
239
240
 
240
- describe('defineStoryCreate / defineStoryUpdate combined with mapi client', () => {
241
- it('should produce a defineStoryCreate result accepted by untyped stories.create', async () => {
242
- const createPayload = defineStoryCreate(_pageComponent, {
243
- name: 'My Page',
244
- content: { headline: 'Hello' },
245
- });
246
-
247
- const client = createManagementApiClient(CLIENT_CONFIG);
248
- await client.stories.create({ body: { story: createPayload } });
249
- });
250
-
251
- it('should produce a defineStoryUpdate result accepted by untyped stories.update', async () => {
252
- const updatePayload = defineStoryUpdate(_pageComponent, {
253
- content: { headline: 'Updated' },
254
- });
255
-
256
- const client = createManagementApiClient(CLIENT_CONFIG);
257
- await client.stories.update(1, { body: { story: updatePayload } });
258
- });
259
-
260
- it('should produce a defineStoryCreate result for hero component accepted by untyped stories.create', async () => {
261
- const createPayload = defineStoryCreate(heroComponent, {
262
- name: 'My Hero',
263
- content: { title: 'Welcome', count: 42, sections: [] },
264
- });
265
-
266
- const client = createManagementApiClient(CLIENT_CONFIG);
267
- await client.stories.create({ body: { story: createPayload } });
268
- });
269
-
241
+ describe('withTypes() write-body narrowing', () => {
270
242
  it('should reject nestable-only component name in create body content', () => {
271
243
  const client = createManagementApiClient(CLIENT_CONFIG).withTypes<StoryblokTypes>();
272
244
  client.stories.create({
@@ -295,12 +267,44 @@ describe('defineStoryCreate / defineStoryUpdate combined with mapi client', () =
295
267
  expectTypeOf(story.content.component).toEqualTypeOf<'page' | 'hero'>();
296
268
 
297
269
  if (story.content.component === 'page') {
298
- const updatePayload = defineStoryUpdate(_pageComponent, {
299
- name: story.name,
300
- content: { headline: story.content.headline ?? 'Default' },
301
- });
302
- expectTypeOf(updatePayload).toMatchTypeOf<{ name?: string | null }>();
270
+ expectTypeOf(story.content.headline).toEqualTypeOf<string | null | undefined>();
303
271
  }
304
272
  }
305
273
  });
306
274
  });
275
+
276
+ describe('createManagementApiClient with .withTypes() — field plugins', () => {
277
+ const _themedComponent = defineBlock({
278
+ name: 'themed',
279
+ is_root: true,
280
+ fields: [
281
+ defineField('bg', { type: 'custom', field_type: 'storyblok-colorpicker' }),
282
+ defineField('legacy', { type: 'custom', field_type: 'unregistered-plugin' }),
283
+ ],
284
+ });
285
+ const _schema = defineSchema({
286
+ blocks: { themedComponent: _themedComponent },
287
+ fieldPlugins: { storyblokColorField },
288
+ });
289
+ type Schema = InferSchema<typeof _schema>;
290
+
291
+ it('resolves a registered custom field to the validator output merged with the plugin envelope', async () => {
292
+ const client = createManagementApiClient(CLIENT_CONFIG).withTypes<Schema>();
293
+ const result = await client.stories.get(123);
294
+ if (result.data?.story && result.data.story.content.component === 'themed') {
295
+ expectTypeOf<NonNullable<typeof result.data.story.content.bg>>().toEqualTypeOf<{
296
+ color: string;
297
+ plugin: string;
298
+ _uid?: string;
299
+ }>();
300
+ }
301
+ });
302
+
303
+ it('leaves an unregistered custom field as PluginFieldValue', async () => {
304
+ const client = createManagementApiClient(CLIENT_CONFIG).withTypes<Schema>();
305
+ const result = await client.stories.get(123);
306
+ if (result.data?.story && result.data.story.content.component === 'themed') {
307
+ expectTypeOf<NonNullable<typeof result.data.story.content.legacy>>().toEqualTypeOf<PluginFieldValue>();
308
+ }
309
+ });
310
+ });
package/test/GUIDE.md CHANGED
@@ -48,7 +48,7 @@ node packages/cli/dist/index.mjs components push \
48
48
 
49
49
  **Always push components before stories.** The CLI validates that each story's `content.component` (and any nested block components) exist in the space. Pushing stories first fails if the referenced components are not yet present.
50
50
 
51
- You can do the same with stories and `defineStory`. **Story filename convention:** the CLI requires files named `{slug}_{uuid}.json` where the part after the last `_` exactly matches `story.uuid` in the JSON. Use hyphens (not underscores) in UUIDs. A mismatch causes Pass 2 of the push (reference mapping + content update) to silently skip all stories.
51
+ You can do the same with stories. **Story filename convention:** the CLI requires files named `{slug}_{uuid}.json` where the part after the last `_` exactly matches `story.uuid` in the JSON. Use hyphens (not underscores) in UUIDs. A mismatch causes Pass 2 of the push (reference mapping + content update) to silently skip all stories.
52
52
 
53
53
  ## Known quirks
54
54
 
@@ -56,4 +56,4 @@ You can do the same with stories and `defineStory`. **Story filename convention:
56
56
  - **MAPI `stories.get` takes a positional ID, not an options object:** `client.stories.get(id)` — there is no `getBySlug`. The `list` response does not include `content`; call `get(id)` for full content.
57
57
  - **MAPI client constructor uses `personalAccessToken`:** `createManagementApiClient({ personalAccessToken: '...', spaceId: ... })`.
58
58
  - **Delete methods are `delete`:** Use `client.RESOURCE.delete(id)`.
59
- - **Each story JSON must have a unique `id`.** `defineStory` defaults `id` to `1`. If multiple story files share the same `id`, the CLI's manifest maps all of them to the same `old_id` entry, causing "slug already taken" errors.
59
+ - **Each story JSON must have a unique `id`.** If multiple story files share the same `id`, the CLI's manifest maps all of them to the same `old_id` entry, causing "slug already taken" errors.