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

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 (300) hide show
  1. package/README.md +3 -1
  2. package/dist/client.cjs +190 -0
  3. package/dist/client.cjs.map +1 -0
  4. package/dist/client.d.cts +632 -0
  5. package/dist/client.d.mts +632 -0
  6. package/dist/client.mjs +189 -0
  7. package/dist/client.mjs.map +1 -0
  8. package/dist/error.cjs.map +1 -1
  9. package/dist/error.d.cts +12 -2
  10. package/dist/error.d.mts +12 -2
  11. package/dist/error.mjs.map +1 -1
  12. package/dist/generated/mapi/_internal.gen.d.cts +541 -0
  13. package/dist/generated/mapi/_internal.gen.d.mts +541 -0
  14. package/dist/generated/{shared → mapi}/client/client.gen.cjs +1 -1
  15. package/dist/generated/mapi/client/client.gen.cjs.map +1 -0
  16. package/dist/generated/{shared → mapi}/client/client.gen.mjs +1 -1
  17. package/dist/generated/mapi/client/client.gen.mjs.map +1 -0
  18. package/dist/generated/{shared → mapi}/client/types.gen.d.cts +1 -1
  19. package/dist/generated/{shared → mapi}/client/types.gen.d.mts +1 -1
  20. package/dist/generated/{shared → mapi}/client/utils.gen.cjs +1 -1
  21. package/dist/generated/mapi/client/utils.gen.cjs.map +1 -0
  22. package/dist/generated/{shared → mapi}/client/utils.gen.d.cts +1 -1
  23. package/dist/generated/{shared → mapi}/client/utils.gen.d.mts +1 -1
  24. package/dist/generated/{shared → mapi}/client/utils.gen.mjs +1 -1
  25. package/dist/generated/mapi/client/utils.gen.mjs.map +1 -0
  26. package/dist/generated/mapi/client.gen.cjs +10 -0
  27. package/dist/generated/mapi/client.gen.cjs.map +1 -0
  28. package/dist/generated/mapi/client.gen.mjs +10 -0
  29. package/dist/generated/mapi/client.gen.mjs.map +1 -0
  30. package/dist/generated/{shared → mapi}/core/auth.gen.cjs +1 -1
  31. package/dist/generated/mapi/core/auth.gen.cjs.map +1 -0
  32. package/dist/generated/{shared → mapi}/core/auth.gen.d.cts +1 -1
  33. package/dist/generated/{shared → mapi}/core/auth.gen.d.mts +1 -1
  34. package/dist/generated/{shared → mapi}/core/auth.gen.mjs +1 -1
  35. package/dist/generated/mapi/core/auth.gen.mjs.map +1 -0
  36. package/dist/generated/{shared → mapi}/core/bodySerializer.gen.cjs +1 -1
  37. package/dist/generated/mapi/core/bodySerializer.gen.cjs.map +1 -0
  38. package/dist/generated/{shared → mapi}/core/bodySerializer.gen.d.cts +1 -1
  39. package/dist/generated/{shared → mapi}/core/bodySerializer.gen.d.mts +1 -1
  40. package/dist/generated/{shared → mapi}/core/bodySerializer.gen.mjs +1 -1
  41. package/dist/generated/mapi/core/bodySerializer.gen.mjs.map +1 -0
  42. package/dist/generated/{shared → mapi}/core/params.gen.cjs +1 -1
  43. package/dist/generated/mapi/core/params.gen.cjs.map +1 -0
  44. package/dist/generated/{shared → mapi}/core/params.gen.mjs +1 -1
  45. package/dist/generated/mapi/core/params.gen.mjs.map +1 -0
  46. package/dist/generated/{shared → mapi}/core/pathSerializer.gen.cjs +1 -1
  47. package/dist/generated/mapi/core/pathSerializer.gen.cjs.map +1 -0
  48. package/dist/generated/{shared → mapi}/core/pathSerializer.gen.d.cts +1 -1
  49. package/dist/generated/{shared → mapi}/core/pathSerializer.gen.d.mts +1 -1
  50. package/dist/generated/{shared → mapi}/core/pathSerializer.gen.mjs +1 -1
  51. package/dist/generated/mapi/core/pathSerializer.gen.mjs.map +1 -0
  52. package/dist/generated/{shared → mapi}/core/serverSentEvents.gen.cjs +1 -1
  53. package/dist/generated/mapi/core/serverSentEvents.gen.cjs.map +1 -0
  54. package/dist/generated/{shared → mapi}/core/serverSentEvents.gen.d.cts +1 -1
  55. package/dist/generated/{shared → mapi}/core/serverSentEvents.gen.d.mts +1 -1
  56. package/dist/generated/{shared → mapi}/core/serverSentEvents.gen.mjs +1 -1
  57. package/dist/generated/mapi/core/serverSentEvents.gen.mjs.map +1 -0
  58. package/dist/generated/{shared → mapi}/core/types.gen.d.cts +1 -1
  59. package/dist/generated/{shared → mapi}/core/types.gen.d.mts +1 -1
  60. package/dist/generated/{shared → mapi}/core/utils.gen.cjs +1 -1
  61. package/dist/generated/mapi/core/utils.gen.cjs.map +1 -0
  62. package/dist/generated/{shared → mapi}/core/utils.gen.mjs +1 -1
  63. package/dist/generated/mapi/core/utils.gen.mjs.map +1 -0
  64. package/dist/generated/mapi/sdk.gen.cjs +1477 -0
  65. package/dist/generated/mapi/sdk.gen.cjs.map +1 -0
  66. package/dist/generated/mapi/sdk.gen.mjs +1399 -0
  67. package/dist/generated/mapi/sdk.gen.mjs.map +1 -0
  68. package/dist/generated/mapi/types-aliased.gen.d.cts +1428 -0
  69. package/dist/generated/mapi/types-aliased.gen.d.mts +1428 -0
  70. package/dist/generated/mapi/types.gen.d.cts +5075 -0
  71. package/dist/generated/mapi/types.gen.d.mts +5075 -0
  72. package/dist/generated/overlay/_internal.gen.d.cts +850 -0
  73. package/dist/generated/overlay/_internal.gen.d.mts +850 -0
  74. package/dist/generated/types/_utils.d.cts +7 -0
  75. package/dist/generated/types/_utils.d.mts +7 -0
  76. package/dist/generated/types/block.d.cts +30 -0
  77. package/dist/generated/types/block.d.mts +30 -0
  78. package/dist/generated/types/field.d.cts +75 -0
  79. package/dist/generated/types/field.d.mts +75 -0
  80. package/dist/generated/types/mapi-story.d.cts +29 -0
  81. package/dist/generated/types/mapi-story.d.mts +29 -0
  82. package/dist/index.cjs +3 -173
  83. package/dist/index.d.cts +13 -433
  84. package/dist/index.d.mts +13 -433
  85. package/dist/index.mjs +2 -171
  86. package/dist/resources/asset-folders.cjs +9 -9
  87. package/dist/resources/asset-folders.cjs.map +1 -1
  88. package/dist/resources/asset-folders.mjs +9 -9
  89. package/dist/resources/asset-folders.mjs.map +1 -1
  90. package/dist/resources/assets.cjs +57 -48
  91. package/dist/resources/assets.cjs.map +1 -1
  92. package/dist/resources/assets.d.cts +23 -27
  93. package/dist/resources/assets.d.mts +23 -27
  94. package/dist/resources/assets.mjs +57 -48
  95. package/dist/resources/assets.mjs.map +1 -1
  96. package/dist/resources/component-folders.cjs +9 -9
  97. package/dist/resources/component-folders.cjs.map +1 -1
  98. package/dist/resources/component-folders.mjs +9 -9
  99. package/dist/resources/component-folders.mjs.map +1 -1
  100. package/dist/resources/components.cjs +28 -20
  101. package/dist/resources/components.cjs.map +1 -1
  102. package/dist/resources/components.d.cts +76 -0
  103. package/dist/resources/components.d.mts +76 -0
  104. package/dist/resources/components.mjs +28 -20
  105. package/dist/resources/components.mjs.map +1 -1
  106. package/dist/resources/datasource-entries.cjs +28 -34
  107. package/dist/resources/datasource-entries.cjs.map +1 -1
  108. package/dist/resources/datasource-entries.mjs +29 -35
  109. package/dist/resources/datasource-entries.mjs.map +1 -1
  110. package/dist/resources/datasources.cjs +27 -9
  111. package/dist/resources/datasources.cjs.map +1 -1
  112. package/dist/resources/datasources.mjs +27 -9
  113. package/dist/resources/datasources.mjs.map +1 -1
  114. package/dist/resources/experiments.cjs +299 -0
  115. package/dist/resources/experiments.cjs.map +1 -0
  116. package/dist/resources/experiments.mjs +299 -0
  117. package/dist/resources/experiments.mjs.map +1 -0
  118. package/dist/resources/internal-tags.cjs +7 -7
  119. package/dist/resources/internal-tags.cjs.map +1 -1
  120. package/dist/resources/internal-tags.mjs +7 -7
  121. package/dist/resources/internal-tags.mjs.map +1 -1
  122. package/dist/resources/presets.cjs +9 -9
  123. package/dist/resources/presets.cjs.map +1 -1
  124. package/dist/resources/presets.mjs +9 -9
  125. package/dist/resources/presets.mjs.map +1 -1
  126. package/dist/resources/shared.cjs +15 -0
  127. package/dist/resources/shared.cjs.map +1 -1
  128. package/dist/resources/shared.mjs +15 -1
  129. package/dist/resources/shared.mjs.map +1 -1
  130. package/dist/resources/spaces.cjs +8 -7
  131. package/dist/resources/spaces.cjs.map +1 -1
  132. package/dist/resources/spaces.d.cts +8 -0
  133. package/dist/resources/spaces.d.mts +7 -0
  134. package/dist/resources/spaces.mjs +8 -7
  135. package/dist/resources/spaces.mjs.map +1 -1
  136. package/dist/resources/stories.cjs +20 -16
  137. package/dist/resources/stories.cjs.map +1 -1
  138. package/dist/resources/stories.d.cts +93 -0
  139. package/dist/resources/stories.d.mts +93 -0
  140. package/dist/resources/stories.mjs +20 -16
  141. package/dist/resources/stories.mjs.map +1 -1
  142. package/dist/resources/users.cjs +3 -3
  143. package/dist/resources/users.cjs.map +1 -1
  144. package/dist/resources/users.mjs +3 -3
  145. package/dist/resources/users.mjs.map +1 -1
  146. package/dist/utils/query-serializer.cjs +54 -0
  147. package/dist/utils/query-serializer.cjs.map +1 -0
  148. package/dist/utils/query-serializer.mjs +54 -0
  149. package/dist/utils/query-serializer.mjs.map +1 -0
  150. package/package.json +16 -14
  151. package/playground/integration-tests/README.md +24 -0
  152. package/playground/integration-tests/eslint.config.js +5 -0
  153. package/playground/integration-tests/node_modules/.bin/eslint +16 -0
  154. package/playground/integration-tests/node_modules/.bin/tsc +16 -0
  155. package/playground/integration-tests/node_modules/.bin/tsserver +16 -0
  156. package/playground/integration-tests/node_modules/.bin/vitest +16 -0
  157. package/playground/integration-tests/package.json +24 -0
  158. package/playground/integration-tests/test/setup.e2e.ts +11 -0
  159. package/playground/integration-tests/test/specs/mapi-round-trip.spec.e2e.ts +520 -0
  160. package/playground/integration-tests/test/types/components.test-d.ts +113 -0
  161. package/playground/integration-tests/test/types/resources.test-d.ts +364 -0
  162. package/playground/integration-tests/test/types/stories.test-d.ts +306 -0
  163. package/playground/integration-tests/vitest.config.e2e.ts +25 -0
  164. package/playground/integration-tests/vitest.config.ts +13 -0
  165. package/test/GUIDE.md +59 -0
  166. package/vitest.config.ts +5 -0
  167. package/dist/generated/asset_folders/client.gen.cjs +0 -10
  168. package/dist/generated/asset_folders/client.gen.cjs.map +0 -1
  169. package/dist/generated/asset_folders/client.gen.mjs +0 -10
  170. package/dist/generated/asset_folders/client.gen.mjs.map +0 -1
  171. package/dist/generated/asset_folders/sdk.gen.cjs +0 -99
  172. package/dist/generated/asset_folders/sdk.gen.cjs.map +0 -1
  173. package/dist/generated/asset_folders/sdk.gen.mjs +0 -95
  174. package/dist/generated/asset_folders/sdk.gen.mjs.map +0 -1
  175. package/dist/generated/asset_folders/types.gen.d.cts +0 -156
  176. package/dist/generated/asset_folders/types.gen.d.mts +0 -156
  177. package/dist/generated/assets/client.gen.cjs +0 -10
  178. package/dist/generated/assets/client.gen.cjs.map +0 -1
  179. package/dist/generated/assets/client.gen.mjs +0 -10
  180. package/dist/generated/assets/client.gen.mjs.map +0 -1
  181. package/dist/generated/assets/sdk.gen.cjs +0 -179
  182. package/dist/generated/assets/sdk.gen.cjs.map +0 -1
  183. package/dist/generated/assets/sdk.gen.mjs +0 -171
  184. package/dist/generated/assets/sdk.gen.mjs.map +0 -1
  185. package/dist/generated/assets/types.gen.d.cts +0 -415
  186. package/dist/generated/assets/types.gen.d.mts +0 -415
  187. package/dist/generated/component_folders/client.gen.cjs +0 -10
  188. package/dist/generated/component_folders/client.gen.cjs.map +0 -1
  189. package/dist/generated/component_folders/client.gen.mjs +0 -10
  190. package/dist/generated/component_folders/client.gen.mjs.map +0 -1
  191. package/dist/generated/component_folders/sdk.gen.cjs +0 -99
  192. package/dist/generated/component_folders/sdk.gen.cjs.map +0 -1
  193. package/dist/generated/component_folders/sdk.gen.mjs +0 -95
  194. package/dist/generated/component_folders/sdk.gen.mjs.map +0 -1
  195. package/dist/generated/component_folders/types.gen.d.cts +0 -118
  196. package/dist/generated/component_folders/types.gen.d.mts +0 -118
  197. package/dist/generated/components/client.gen.cjs +0 -10
  198. package/dist/generated/components/client.gen.cjs.map +0 -1
  199. package/dist/generated/components/client.gen.mjs +0 -10
  200. package/dist/generated/components/client.gen.mjs.map +0 -1
  201. package/dist/generated/components/sdk.gen.cjs +0 -171
  202. package/dist/generated/components/sdk.gen.cjs.map +0 -1
  203. package/dist/generated/components/sdk.gen.mjs +0 -163
  204. package/dist/generated/components/sdk.gen.mjs.map +0 -1
  205. package/dist/generated/components/types.gen.d.cts +0 -855
  206. package/dist/generated/components/types.gen.d.mts +0 -855
  207. package/dist/generated/datasource_entries/client.gen.cjs +0 -10
  208. package/dist/generated/datasource_entries/client.gen.cjs.map +0 -1
  209. package/dist/generated/datasource_entries/client.gen.mjs +0 -10
  210. package/dist/generated/datasource_entries/client.gen.mjs.map +0 -1
  211. package/dist/generated/datasource_entries/sdk.gen.cjs +0 -89
  212. package/dist/generated/datasource_entries/sdk.gen.cjs.map +0 -1
  213. package/dist/generated/datasource_entries/sdk.gen.mjs +0 -85
  214. package/dist/generated/datasource_entries/sdk.gen.mjs.map +0 -1
  215. package/dist/generated/datasource_entries/types.gen.d.cts +0 -156
  216. package/dist/generated/datasource_entries/types.gen.d.mts +0 -156
  217. package/dist/generated/datasources/client.gen.cjs +0 -10
  218. package/dist/generated/datasources/client.gen.cjs.map +0 -1
  219. package/dist/generated/datasources/client.gen.mjs +0 -10
  220. package/dist/generated/datasources/client.gen.mjs.map +0 -1
  221. package/dist/generated/datasources/sdk.gen.cjs +0 -89
  222. package/dist/generated/datasources/sdk.gen.cjs.map +0 -1
  223. package/dist/generated/datasources/sdk.gen.mjs +0 -85
  224. package/dist/generated/datasources/sdk.gen.mjs.map +0 -1
  225. package/dist/generated/datasources/types.gen.d.cts +0 -186
  226. package/dist/generated/datasources/types.gen.d.mts +0 -186
  227. package/dist/generated/internal_tags/client.gen.cjs +0 -10
  228. package/dist/generated/internal_tags/client.gen.cjs.map +0 -1
  229. package/dist/generated/internal_tags/client.gen.mjs +0 -10
  230. package/dist/generated/internal_tags/client.gen.mjs.map +0 -1
  231. package/dist/generated/internal_tags/sdk.gen.cjs +0 -74
  232. package/dist/generated/internal_tags/sdk.gen.cjs.map +0 -1
  233. package/dist/generated/internal_tags/sdk.gen.mjs +0 -71
  234. package/dist/generated/internal_tags/sdk.gen.mjs.map +0 -1
  235. package/dist/generated/internal_tags/types.gen.d.cts +0 -106
  236. package/dist/generated/internal_tags/types.gen.d.mts +0 -106
  237. package/dist/generated/presets/client.gen.cjs +0 -10
  238. package/dist/generated/presets/client.gen.cjs.map +0 -1
  239. package/dist/generated/presets/client.gen.mjs +0 -10
  240. package/dist/generated/presets/client.gen.mjs.map +0 -1
  241. package/dist/generated/presets/sdk.gen.cjs +0 -99
  242. package/dist/generated/presets/sdk.gen.cjs.map +0 -1
  243. package/dist/generated/presets/sdk.gen.mjs +0 -95
  244. package/dist/generated/presets/sdk.gen.mjs.map +0 -1
  245. package/dist/generated/presets/types.gen.d.cts +0 -176
  246. package/dist/generated/presets/types.gen.d.mts +0 -176
  247. package/dist/generated/shared/client/client.gen.cjs.map +0 -1
  248. package/dist/generated/shared/client/client.gen.mjs.map +0 -1
  249. package/dist/generated/shared/client/utils.gen.cjs.map +0 -1
  250. package/dist/generated/shared/client/utils.gen.mjs.map +0 -1
  251. package/dist/generated/shared/core/auth.gen.cjs.map +0 -1
  252. package/dist/generated/shared/core/auth.gen.mjs.map +0 -1
  253. package/dist/generated/shared/core/bodySerializer.gen.cjs.map +0 -1
  254. package/dist/generated/shared/core/bodySerializer.gen.mjs.map +0 -1
  255. package/dist/generated/shared/core/params.gen.cjs.map +0 -1
  256. package/dist/generated/shared/core/params.gen.mjs.map +0 -1
  257. package/dist/generated/shared/core/pathSerializer.gen.cjs.map +0 -1
  258. package/dist/generated/shared/core/pathSerializer.gen.mjs.map +0 -1
  259. package/dist/generated/shared/core/serverSentEvents.gen.cjs.map +0 -1
  260. package/dist/generated/shared/core/serverSentEvents.gen.mjs.map +0 -1
  261. package/dist/generated/shared/core/utils.gen.cjs.map +0 -1
  262. package/dist/generated/shared/core/utils.gen.mjs.map +0 -1
  263. package/dist/generated/spaces/client.gen.cjs +0 -10
  264. package/dist/generated/spaces/client.gen.cjs.map +0 -1
  265. package/dist/generated/spaces/client.gen.mjs +0 -10
  266. package/dist/generated/spaces/client.gen.mjs.map +0 -1
  267. package/dist/generated/spaces/sdk.gen.cjs +0 -99
  268. package/dist/generated/spaces/sdk.gen.cjs.map +0 -1
  269. package/dist/generated/spaces/sdk.gen.mjs +0 -95
  270. package/dist/generated/spaces/sdk.gen.mjs.map +0 -1
  271. package/dist/generated/spaces/types.gen.d.cts +0 -544
  272. package/dist/generated/spaces/types.gen.d.mts +0 -544
  273. package/dist/generated/stories/client.gen.cjs +0 -10
  274. package/dist/generated/stories/client.gen.cjs.map +0 -1
  275. package/dist/generated/stories/client.gen.mjs +0 -10
  276. package/dist/generated/stories/client.gen.mjs.map +0 -1
  277. package/dist/generated/stories/sdk.gen.cjs +0 -138
  278. package/dist/generated/stories/sdk.gen.cjs.map +0 -1
  279. package/dist/generated/stories/sdk.gen.mjs +0 -131
  280. package/dist/generated/stories/sdk.gen.mjs.map +0 -1
  281. package/dist/generated/stories/types.gen.d.cts +0 -1306
  282. package/dist/generated/stories/types.gen.d.mts +0 -1306
  283. package/dist/generated/users/client.gen.cjs +0 -10
  284. package/dist/generated/users/client.gen.cjs.map +0 -1
  285. package/dist/generated/users/client.gen.mjs +0 -10
  286. package/dist/generated/users/client.gen.mjs.map +0 -1
  287. package/dist/generated/users/sdk.gen.cjs +0 -44
  288. package/dist/generated/users/sdk.gen.cjs.map +0 -1
  289. package/dist/generated/users/sdk.gen.mjs +0 -43
  290. package/dist/generated/users/sdk.gen.mjs.map +0 -1
  291. package/dist/generated/users/types.gen.d.cts +0 -348
  292. package/dist/generated/users/types.gen.d.mts +0 -348
  293. package/dist/index.cjs.map +0 -1
  294. package/dist/index.mjs.map +0 -1
  295. package/dist/types.d.cts +0 -130
  296. package/dist/types.d.mts +0 -130
  297. /package/dist/generated/{shared → mapi}/client/client.gen.d.mts +0 -0
  298. /package/dist/generated/{shared → mapi}/client/index.cjs +0 -0
  299. /package/dist/generated/{shared → mapi}/client/index.d.mts +0 -0
  300. /package/dist/generated/{shared → mapi}/client/index.mjs +0 -0
@@ -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/mapi-integration-tests 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,113 @@
1
+ import { defineBlock, defineBlockCreate, defineBlockUpdate, defineField } from '@storyblok/schema';
2
+ import { type Component as ComponentMapi, createManagementApiClient } from '@storyblok/management-api-client';
3
+ import { describe, expectTypeOf, it } from 'vitest';
4
+
5
+ // Nestable block — not a root story type
6
+ const teaserComponent = defineBlock({
7
+ name: 'teaser',
8
+ schema: [
9
+ defineField('text', { type: 'text' }),
10
+ defineField('image', { type: 'asset' }),
11
+ ],
12
+ });
13
+
14
+ // Root content type, not nestable
15
+ const _pageComponent = defineBlock({
16
+ name: 'page',
17
+ is_root: true,
18
+ is_nestable: false,
19
+ schema: [
20
+ defineField('headline', { type: 'text', required: true }),
21
+ defineField('body', { type: 'richtext' }),
22
+ defineField('teasers', { type: 'bloks', component_whitelist: [teaserComponent.name] }),
23
+ ],
24
+ });
25
+
26
+ const CLIENT_CONFIG = { personalAccessToken: 'test-token', spaceId: 12345 };
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
+ describe('components.get response shape', () => {
56
+ it('should return a Component from components.get matching the wire shape', async () => {
57
+ const client = createManagementApiClient(CLIENT_CONFIG);
58
+ const result = await client.components.get(123);
59
+
60
+ if (result.data?.component) {
61
+ // components.get() returns the wrapper Component (= Block), matching the wire shape
62
+ expectTypeOf(result.data.component.id).toEqualTypeOf<ComponentMapi['id']>();
63
+ expectTypeOf(result.data.component.name).toEqualTypeOf<ComponentMapi['name']>();
64
+ expectTypeOf(result.data.component).toHaveProperty('schema');
65
+ }
66
+ });
67
+ });
68
+
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
+ describe('defineBlock result used in .withTypes() interface', () => {
83
+ interface StoryblokTypes {
84
+ components: typeof _pageComponent | typeof teaserComponent;
85
+ }
86
+
87
+ it('should narrow story content to page or teaser after withTypes', async () => {
88
+ const client = createManagementApiClient(CLIENT_CONFIG).withTypes<StoryblokTypes>();
89
+ const result = await client.stories.get(123);
90
+
91
+ if (result.data?.story) {
92
+ // Only page is a root component (is_root: true); teaser is nestable-only
93
+ expectTypeOf(result.data.story.content.component).toEqualTypeOf<'page'>();
94
+ }
95
+ });
96
+
97
+ it('should narrow bloks field on page to whitelisted teasers', async () => {
98
+ const client = createManagementApiClient(CLIENT_CONFIG).withTypes<StoryblokTypes>();
99
+ const result = await client.stories.get(123);
100
+
101
+ if (result.data?.story) {
102
+ const story = result.data.story;
103
+ if (story.content.component === 'page') {
104
+ if (story.content.teasers) {
105
+ for (const teaser of story.content.teasers) {
106
+ // teasers whitelists only the teaser component
107
+ expectTypeOf(teaser.component).toEqualTypeOf<'teaser'>();
108
+ }
109
+ }
110
+ }
111
+ }
112
+ });
113
+ });