@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.
- package/README.md +3 -1
- package/dist/client.cjs +190 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +632 -0
- package/dist/client.d.mts +632 -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/mapi/_internal.gen.d.cts +541 -0
- package/dist/generated/mapi/_internal.gen.d.mts +541 -0
- package/dist/generated/{shared → mapi}/client/client.gen.cjs +1 -1
- package/dist/generated/mapi/client/client.gen.cjs.map +1 -0
- package/dist/generated/{shared → mapi}/client/client.gen.mjs +1 -1
- package/dist/generated/mapi/client/client.gen.mjs.map +1 -0
- package/dist/generated/{shared → mapi}/client/types.gen.d.cts +1 -1
- package/dist/generated/{shared → mapi}/client/types.gen.d.mts +1 -1
- package/dist/generated/{shared → mapi}/client/utils.gen.cjs +1 -1
- package/dist/generated/mapi/client/utils.gen.cjs.map +1 -0
- package/dist/generated/{shared → mapi}/client/utils.gen.d.cts +1 -1
- package/dist/generated/{shared → mapi}/client/utils.gen.d.mts +1 -1
- package/dist/generated/{shared → mapi}/client/utils.gen.mjs +1 -1
- package/dist/generated/mapi/client/utils.gen.mjs.map +1 -0
- package/dist/generated/mapi/client.gen.cjs +10 -0
- package/dist/generated/mapi/client.gen.cjs.map +1 -0
- package/dist/generated/mapi/client.gen.mjs +10 -0
- package/dist/generated/mapi/client.gen.mjs.map +1 -0
- package/dist/generated/{shared → mapi}/core/auth.gen.cjs +1 -1
- package/dist/generated/mapi/core/auth.gen.cjs.map +1 -0
- package/dist/generated/{shared → mapi}/core/auth.gen.d.cts +1 -1
- package/dist/generated/{shared → mapi}/core/auth.gen.d.mts +1 -1
- package/dist/generated/{shared → mapi}/core/auth.gen.mjs +1 -1
- package/dist/generated/mapi/core/auth.gen.mjs.map +1 -0
- package/dist/generated/{shared → mapi}/core/bodySerializer.gen.cjs +1 -1
- package/dist/generated/mapi/core/bodySerializer.gen.cjs.map +1 -0
- package/dist/generated/{shared → mapi}/core/bodySerializer.gen.d.cts +1 -1
- package/dist/generated/{shared → mapi}/core/bodySerializer.gen.d.mts +1 -1
- package/dist/generated/{shared → mapi}/core/bodySerializer.gen.mjs +1 -1
- package/dist/generated/mapi/core/bodySerializer.gen.mjs.map +1 -0
- package/dist/generated/{shared → mapi}/core/params.gen.cjs +1 -1
- package/dist/generated/mapi/core/params.gen.cjs.map +1 -0
- package/dist/generated/{shared → mapi}/core/params.gen.mjs +1 -1
- package/dist/generated/mapi/core/params.gen.mjs.map +1 -0
- package/dist/generated/{shared → mapi}/core/pathSerializer.gen.cjs +1 -1
- package/dist/generated/mapi/core/pathSerializer.gen.cjs.map +1 -0
- package/dist/generated/{shared → mapi}/core/pathSerializer.gen.d.cts +1 -1
- package/dist/generated/{shared → mapi}/core/pathSerializer.gen.d.mts +1 -1
- package/dist/generated/{shared → mapi}/core/pathSerializer.gen.mjs +1 -1
- package/dist/generated/mapi/core/pathSerializer.gen.mjs.map +1 -0
- package/dist/generated/{shared → mapi}/core/serverSentEvents.gen.cjs +1 -1
- package/dist/generated/mapi/core/serverSentEvents.gen.cjs.map +1 -0
- package/dist/generated/{shared → mapi}/core/serverSentEvents.gen.d.cts +1 -1
- package/dist/generated/{shared → mapi}/core/serverSentEvents.gen.d.mts +1 -1
- package/dist/generated/{shared → mapi}/core/serverSentEvents.gen.mjs +1 -1
- package/dist/generated/mapi/core/serverSentEvents.gen.mjs.map +1 -0
- package/dist/generated/{shared → mapi}/core/types.gen.d.cts +1 -1
- package/dist/generated/{shared → mapi}/core/types.gen.d.mts +1 -1
- package/dist/generated/{shared → mapi}/core/utils.gen.cjs +1 -1
- package/dist/generated/mapi/core/utils.gen.cjs.map +1 -0
- package/dist/generated/{shared → mapi}/core/utils.gen.mjs +1 -1
- package/dist/generated/mapi/core/utils.gen.mjs.map +1 -0
- package/dist/generated/mapi/sdk.gen.cjs +1477 -0
- package/dist/generated/mapi/sdk.gen.cjs.map +1 -0
- package/dist/generated/mapi/sdk.gen.mjs +1399 -0
- package/dist/generated/mapi/sdk.gen.mjs.map +1 -0
- package/dist/generated/mapi/types-aliased.gen.d.cts +1428 -0
- package/dist/generated/mapi/types-aliased.gen.d.mts +1428 -0
- package/dist/generated/mapi/types.gen.d.cts +5075 -0
- package/dist/generated/mapi/types.gen.d.mts +5075 -0
- package/dist/generated/overlay/_internal.gen.d.cts +850 -0
- package/dist/generated/overlay/_internal.gen.d.mts +850 -0
- package/dist/generated/types/_utils.d.cts +7 -0
- package/dist/generated/types/_utils.d.mts +7 -0
- package/dist/generated/types/block.d.cts +30 -0
- package/dist/generated/types/block.d.mts +30 -0
- package/dist/generated/types/field.d.cts +75 -0
- package/dist/generated/types/field.d.mts +75 -0
- package/dist/generated/types/mapi-story.d.cts +29 -0
- package/dist/generated/types/mapi-story.d.mts +29 -0
- package/dist/index.cjs +3 -173
- package/dist/index.d.cts +13 -433
- package/dist/index.d.mts +13 -433
- package/dist/index.mjs +2 -171
- package/dist/resources/asset-folders.cjs +9 -9
- package/dist/resources/asset-folders.cjs.map +1 -1
- package/dist/resources/asset-folders.mjs +9 -9
- package/dist/resources/asset-folders.mjs.map +1 -1
- package/dist/resources/assets.cjs +57 -48
- package/dist/resources/assets.cjs.map +1 -1
- package/dist/resources/assets.d.cts +23 -27
- package/dist/resources/assets.d.mts +23 -27
- package/dist/resources/assets.mjs +57 -48
- package/dist/resources/assets.mjs.map +1 -1
- package/dist/resources/component-folders.cjs +9 -9
- package/dist/resources/component-folders.cjs.map +1 -1
- package/dist/resources/component-folders.mjs +9 -9
- package/dist/resources/component-folders.mjs.map +1 -1
- package/dist/resources/components.cjs +28 -20
- package/dist/resources/components.cjs.map +1 -1
- package/dist/resources/components.d.cts +76 -0
- package/dist/resources/components.d.mts +76 -0
- package/dist/resources/components.mjs +28 -20
- package/dist/resources/components.mjs.map +1 -1
- package/dist/resources/datasource-entries.cjs +28 -34
- package/dist/resources/datasource-entries.cjs.map +1 -1
- package/dist/resources/datasource-entries.mjs +29 -35
- package/dist/resources/datasource-entries.mjs.map +1 -1
- package/dist/resources/datasources.cjs +27 -9
- package/dist/resources/datasources.cjs.map +1 -1
- package/dist/resources/datasources.mjs +27 -9
- package/dist/resources/datasources.mjs.map +1 -1
- package/dist/resources/experiments.cjs +299 -0
- package/dist/resources/experiments.cjs.map +1 -0
- package/dist/resources/experiments.mjs +299 -0
- package/dist/resources/experiments.mjs.map +1 -0
- package/dist/resources/internal-tags.cjs +7 -7
- package/dist/resources/internal-tags.cjs.map +1 -1
- package/dist/resources/internal-tags.mjs +7 -7
- package/dist/resources/internal-tags.mjs.map +1 -1
- package/dist/resources/presets.cjs +9 -9
- package/dist/resources/presets.cjs.map +1 -1
- package/dist/resources/presets.mjs +9 -9
- package/dist/resources/presets.mjs.map +1 -1
- package/dist/resources/shared.cjs +15 -0
- package/dist/resources/shared.cjs.map +1 -1
- package/dist/resources/shared.mjs +15 -1
- package/dist/resources/shared.mjs.map +1 -1
- package/dist/resources/spaces.cjs +8 -7
- package/dist/resources/spaces.cjs.map +1 -1
- package/dist/resources/spaces.d.cts +8 -0
- package/dist/resources/spaces.d.mts +7 -0
- package/dist/resources/spaces.mjs +8 -7
- package/dist/resources/spaces.mjs.map +1 -1
- package/dist/resources/stories.cjs +20 -16
- package/dist/resources/stories.cjs.map +1 -1
- package/dist/resources/stories.d.cts +93 -0
- package/dist/resources/stories.d.mts +93 -0
- package/dist/resources/stories.mjs +20 -16
- package/dist/resources/stories.mjs.map +1 -1
- package/dist/resources/users.cjs +3 -3
- package/dist/resources/users.cjs.map +1 -1
- package/dist/resources/users.mjs +3 -3
- package/dist/resources/users.mjs.map +1 -1
- package/dist/utils/query-serializer.cjs +54 -0
- package/dist/utils/query-serializer.cjs.map +1 -0
- package/dist/utils/query-serializer.mjs +54 -0
- package/dist/utils/query-serializer.mjs.map +1 -0
- package/package.json +16 -14
- package/playground/integration-tests/README.md +24 -0
- package/playground/integration-tests/eslint.config.js +5 -0
- package/playground/integration-tests/node_modules/.bin/eslint +16 -0
- package/playground/integration-tests/node_modules/.bin/tsc +16 -0
- package/playground/integration-tests/node_modules/.bin/tsserver +16 -0
- package/playground/integration-tests/node_modules/.bin/vitest +16 -0
- package/playground/integration-tests/package.json +24 -0
- package/playground/integration-tests/test/setup.e2e.ts +11 -0
- package/playground/integration-tests/test/specs/mapi-round-trip.spec.e2e.ts +520 -0
- package/playground/integration-tests/test/types/components.test-d.ts +113 -0
- package/playground/integration-tests/test/types/resources.test-d.ts +364 -0
- package/playground/integration-tests/test/types/stories.test-d.ts +306 -0
- package/playground/integration-tests/vitest.config.e2e.ts +25 -0
- package/playground/integration-tests/vitest.config.ts +13 -0
- package/test/GUIDE.md +59 -0
- package/vitest.config.ts +5 -0
- package/dist/generated/asset_folders/client.gen.cjs +0 -10
- package/dist/generated/asset_folders/client.gen.cjs.map +0 -1
- package/dist/generated/asset_folders/client.gen.mjs +0 -10
- package/dist/generated/asset_folders/client.gen.mjs.map +0 -1
- package/dist/generated/asset_folders/sdk.gen.cjs +0 -99
- package/dist/generated/asset_folders/sdk.gen.cjs.map +0 -1
- package/dist/generated/asset_folders/sdk.gen.mjs +0 -95
- package/dist/generated/asset_folders/sdk.gen.mjs.map +0 -1
- package/dist/generated/asset_folders/types.gen.d.cts +0 -156
- package/dist/generated/asset_folders/types.gen.d.mts +0 -156
- package/dist/generated/assets/client.gen.cjs +0 -10
- package/dist/generated/assets/client.gen.cjs.map +0 -1
- package/dist/generated/assets/client.gen.mjs +0 -10
- package/dist/generated/assets/client.gen.mjs.map +0 -1
- package/dist/generated/assets/sdk.gen.cjs +0 -179
- package/dist/generated/assets/sdk.gen.cjs.map +0 -1
- package/dist/generated/assets/sdk.gen.mjs +0 -171
- package/dist/generated/assets/sdk.gen.mjs.map +0 -1
- package/dist/generated/assets/types.gen.d.cts +0 -415
- package/dist/generated/assets/types.gen.d.mts +0 -415
- package/dist/generated/component_folders/client.gen.cjs +0 -10
- package/dist/generated/component_folders/client.gen.cjs.map +0 -1
- package/dist/generated/component_folders/client.gen.mjs +0 -10
- package/dist/generated/component_folders/client.gen.mjs.map +0 -1
- package/dist/generated/component_folders/sdk.gen.cjs +0 -99
- package/dist/generated/component_folders/sdk.gen.cjs.map +0 -1
- package/dist/generated/component_folders/sdk.gen.mjs +0 -95
- package/dist/generated/component_folders/sdk.gen.mjs.map +0 -1
- package/dist/generated/component_folders/types.gen.d.cts +0 -118
- package/dist/generated/component_folders/types.gen.d.mts +0 -118
- package/dist/generated/components/client.gen.cjs +0 -10
- package/dist/generated/components/client.gen.cjs.map +0 -1
- package/dist/generated/components/client.gen.mjs +0 -10
- package/dist/generated/components/client.gen.mjs.map +0 -1
- package/dist/generated/components/sdk.gen.cjs +0 -171
- package/dist/generated/components/sdk.gen.cjs.map +0 -1
- package/dist/generated/components/sdk.gen.mjs +0 -163
- package/dist/generated/components/sdk.gen.mjs.map +0 -1
- package/dist/generated/components/types.gen.d.cts +0 -855
- package/dist/generated/components/types.gen.d.mts +0 -855
- package/dist/generated/datasource_entries/client.gen.cjs +0 -10
- package/dist/generated/datasource_entries/client.gen.cjs.map +0 -1
- package/dist/generated/datasource_entries/client.gen.mjs +0 -10
- package/dist/generated/datasource_entries/client.gen.mjs.map +0 -1
- package/dist/generated/datasource_entries/sdk.gen.cjs +0 -89
- package/dist/generated/datasource_entries/sdk.gen.cjs.map +0 -1
- package/dist/generated/datasource_entries/sdk.gen.mjs +0 -85
- package/dist/generated/datasource_entries/sdk.gen.mjs.map +0 -1
- package/dist/generated/datasource_entries/types.gen.d.cts +0 -156
- package/dist/generated/datasource_entries/types.gen.d.mts +0 -156
- package/dist/generated/datasources/client.gen.cjs +0 -10
- package/dist/generated/datasources/client.gen.cjs.map +0 -1
- package/dist/generated/datasources/client.gen.mjs +0 -10
- package/dist/generated/datasources/client.gen.mjs.map +0 -1
- package/dist/generated/datasources/sdk.gen.cjs +0 -89
- package/dist/generated/datasources/sdk.gen.cjs.map +0 -1
- package/dist/generated/datasources/sdk.gen.mjs +0 -85
- package/dist/generated/datasources/sdk.gen.mjs.map +0 -1
- package/dist/generated/datasources/types.gen.d.cts +0 -186
- package/dist/generated/datasources/types.gen.d.mts +0 -186
- package/dist/generated/internal_tags/client.gen.cjs +0 -10
- package/dist/generated/internal_tags/client.gen.cjs.map +0 -1
- package/dist/generated/internal_tags/client.gen.mjs +0 -10
- package/dist/generated/internal_tags/client.gen.mjs.map +0 -1
- package/dist/generated/internal_tags/sdk.gen.cjs +0 -74
- package/dist/generated/internal_tags/sdk.gen.cjs.map +0 -1
- package/dist/generated/internal_tags/sdk.gen.mjs +0 -71
- package/dist/generated/internal_tags/sdk.gen.mjs.map +0 -1
- package/dist/generated/internal_tags/types.gen.d.cts +0 -106
- package/dist/generated/internal_tags/types.gen.d.mts +0 -106
- package/dist/generated/presets/client.gen.cjs +0 -10
- package/dist/generated/presets/client.gen.cjs.map +0 -1
- package/dist/generated/presets/client.gen.mjs +0 -10
- package/dist/generated/presets/client.gen.mjs.map +0 -1
- package/dist/generated/presets/sdk.gen.cjs +0 -99
- package/dist/generated/presets/sdk.gen.cjs.map +0 -1
- package/dist/generated/presets/sdk.gen.mjs +0 -95
- package/dist/generated/presets/sdk.gen.mjs.map +0 -1
- package/dist/generated/presets/types.gen.d.cts +0 -176
- package/dist/generated/presets/types.gen.d.mts +0 -176
- package/dist/generated/shared/client/client.gen.cjs.map +0 -1
- package/dist/generated/shared/client/client.gen.mjs.map +0 -1
- package/dist/generated/shared/client/utils.gen.cjs.map +0 -1
- package/dist/generated/shared/client/utils.gen.mjs.map +0 -1
- package/dist/generated/shared/core/auth.gen.cjs.map +0 -1
- package/dist/generated/shared/core/auth.gen.mjs.map +0 -1
- package/dist/generated/shared/core/bodySerializer.gen.cjs.map +0 -1
- package/dist/generated/shared/core/bodySerializer.gen.mjs.map +0 -1
- package/dist/generated/shared/core/params.gen.cjs.map +0 -1
- package/dist/generated/shared/core/params.gen.mjs.map +0 -1
- package/dist/generated/shared/core/pathSerializer.gen.cjs.map +0 -1
- package/dist/generated/shared/core/pathSerializer.gen.mjs.map +0 -1
- package/dist/generated/shared/core/serverSentEvents.gen.cjs.map +0 -1
- package/dist/generated/shared/core/serverSentEvents.gen.mjs.map +0 -1
- package/dist/generated/shared/core/utils.gen.cjs.map +0 -1
- package/dist/generated/shared/core/utils.gen.mjs.map +0 -1
- package/dist/generated/spaces/client.gen.cjs +0 -10
- package/dist/generated/spaces/client.gen.cjs.map +0 -1
- package/dist/generated/spaces/client.gen.mjs +0 -10
- package/dist/generated/spaces/client.gen.mjs.map +0 -1
- package/dist/generated/spaces/sdk.gen.cjs +0 -99
- package/dist/generated/spaces/sdk.gen.cjs.map +0 -1
- package/dist/generated/spaces/sdk.gen.mjs +0 -95
- package/dist/generated/spaces/sdk.gen.mjs.map +0 -1
- package/dist/generated/spaces/types.gen.d.cts +0 -544
- package/dist/generated/spaces/types.gen.d.mts +0 -544
- package/dist/generated/stories/client.gen.cjs +0 -10
- package/dist/generated/stories/client.gen.cjs.map +0 -1
- package/dist/generated/stories/client.gen.mjs +0 -10
- package/dist/generated/stories/client.gen.mjs.map +0 -1
- package/dist/generated/stories/sdk.gen.cjs +0 -138
- package/dist/generated/stories/sdk.gen.cjs.map +0 -1
- package/dist/generated/stories/sdk.gen.mjs +0 -131
- package/dist/generated/stories/sdk.gen.mjs.map +0 -1
- package/dist/generated/stories/types.gen.d.cts +0 -1306
- package/dist/generated/stories/types.gen.d.mts +0 -1306
- package/dist/generated/users/client.gen.cjs +0 -10
- package/dist/generated/users/client.gen.cjs.map +0 -1
- package/dist/generated/users/client.gen.mjs +0 -10
- package/dist/generated/users/client.gen.mjs.map +0 -1
- package/dist/generated/users/sdk.gen.cjs +0 -44
- package/dist/generated/users/sdk.gen.cjs.map +0 -1
- package/dist/generated/users/sdk.gen.mjs +0 -43
- package/dist/generated/users/sdk.gen.mjs.map +0 -1
- package/dist/generated/users/types.gen.d.cts +0 -348
- package/dist/generated/users/types.gen.d.mts +0 -348
- 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
- /package/dist/generated/{shared → mapi}/client/client.gen.d.mts +0 -0
- /package/dist/generated/{shared → mapi}/client/index.cjs +0 -0
- /package/dist/generated/{shared → mapi}/client/index.d.mts +0 -0
- /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
|
+
});
|