Skip to content

Commit 375bdc9

Browse files
committed
chore: add OpenAPI and MAPI client (#227)
TypeScript SDK for the Storyblok Management API featuring OpenAPI specifications, generated type-safe clients. - Complete API definitions for all MAPI resources (stories, datasources, components, spaces, etc.) - Detailed schemas, parameters, and response types - Authentication schemes (Personal Access Token, OAuth) - Multi-region server configurations - Auto-generated SDKs from OpenAPI specs using `@hey-api/openapi-ts` - Centralized MapiClient with nested resource SDKs - Full TypeScript IntelliSense and compile-time safety - Shared HTTP client instance across all resources - Rate limiting with 429 retry handling - Automatic region resolution based on space ID ```typescript const client = new MapiClient({ token: { accessToken: 'your-token' }, rateLimiting: { maxConcurrent: 5, retryDelay: 1000 } // optional }); // Fully typed API calls with automatic region resolution await client.stories.list({ path: { space_id: 123456 } }); await client.datasources.create({ path: { space_id: 123456 }, body: {...} }); ``` resolves WDX-70
1 parent 879b9b5 commit 375bdc9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+9451
-448
lines changed

.nxignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ commitlint.config.js
99
LICENSE
1010
CONTRIBUTING.md
1111
README.md
12-
RELEASING.md
12+
RELEASING.md

nx.json

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,22 +25,6 @@
2525
},
2626
"release": {
2727
"projectsRelationship": "independent",
28-
"groups": {
29-
"core-js": {
30-
"projects": [
31-
"storyblok-js-client",
32-
"js",
33-
"richtext",
34-
"storyblok",
35-
"eslint-config"
36-
],
37-
"projectsRelationship": "independent"
38-
},
39-
"sdk": {
40-
"projects": ["react", "vue", "astro", "svelte", "nuxt"],
41-
"projectsRelationship": "independent"
42-
}
43-
},
4428
"version": {
4529
"conventionalCommits": true,
4630
"fallbackCurrentVersionResolver": "disk",

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@
1919
"devDependencies": {
2020
"@commitlint/cli": "^19.8.1",
2121
"@commitlint/config-conventional": "^19.8.1",
22-
"@nx/js": "^21.0.3",
22+
"@nx/js": "21.3.11",
2323
"husky": "^9.1.7",
2424
"lint-staged": "^15.5.2",
25-
"nx": "21.0.3",
25+
"nx": "21.3.11",
2626
"typescript": "5.8.3"
2727
},
2828
"resolutions": {

packages/mapi-client/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
generated
2+
*.generated.ts

packages/mapi-client/README.md

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
# @storyblok/management-api-client
2+
3+
A comprehensive TypeScript SDK for the Storyblok Management API with automatic region resolution and built-in retry logic.
4+
5+
## Features
6+
7+
- **Type-Safe**: Generated from OpenAPI specifications with full TypeScript support
8+
- **Region Resolution**: Automatic regional endpoint selection based on space ID
9+
- **Retry Logic**: Built-in retry with exponential backoff and `retry-after` header support
10+
- **Multi-Resource**: Unified client for many MAPI resources (stories, datasources, components, etc.)
11+
12+
## Installation
13+
14+
```bash
15+
npm install @storyblok/management-api-client
16+
# or
17+
pnpm add @storyblok/management-api-client
18+
```
19+
20+
## Quick Start
21+
22+
```typescript
23+
import { ManagementApiClient } from '@storyblok/management-api-client';
24+
25+
const client = new ManagementApiClient({
26+
token: { accessToken: 'your-personal-access-token' },
27+
// Optional configuration
28+
region: 'us', // 'eu' | 'us' | 'ap' | 'ca' | 'cn'
29+
});
30+
31+
// Get stories with full type safety
32+
const stories = await client.stories.list({
33+
path: { space_id: 123456 },
34+
query: { per_page: 10 }
35+
});
36+
37+
// Create a datasource
38+
const datasource = await client.datasources.create({
39+
path: { space_id: 123456 },
40+
body: {
41+
datasource: {
42+
name: 'My Datasource',
43+
slug: 'my-datasource'
44+
}
45+
}
46+
});
47+
```
48+
49+
## Authentication
50+
51+
### Personal Access Token
52+
```typescript
53+
const client = new ManagementApiClient({
54+
token: { accessToken: 'your-personal-access-token' }
55+
});
56+
```
57+
58+
### OAuth Token
59+
```typescript
60+
const client = new ManagementApiClient({
61+
token: { oauthToken: 'your-oauth-token' }
62+
});
63+
```
64+
65+
## Configuration
66+
67+
```typescript
68+
interface ManagementApiClientConfig {
69+
token: { accessToken: string } | { oauthToken: string };
70+
region?: 'eu' | 'us' | 'ap' | 'ca' | 'cn'; // Auto-detected from space_id if not provided
71+
baseUrl?: string; // Override automatic region resolution
72+
headers?: Record<string, string>; // Additional headers
73+
throwOnError?: boolean; // Throw on HTTP errors (default: false)
74+
}
75+
```
76+
77+
## Available Resources
78+
79+
The client provides access to all Storyblok Management API resources:
80+
81+
```typescript
82+
// Stories
83+
await client.stories.list({ path: { space_id } });
84+
await client.stories.get({ path: { space_id, story_id } });
85+
await client.stories.create({ path: { space_id }, body: { story: {...} } });
86+
await client.stories.updateStory({ path: { space_id, story_id }, body: { story: {...} } });
87+
await client.stories.delete({ path: { space_id, story_id } });
88+
89+
// Datasources
90+
await client.datasources.list({ path: { space_id } });
91+
await client.datasources.create({ path: { space_id }, body: { datasource: {...} } });
92+
93+
// Components
94+
await client.components.list({ path: { space_id } });
95+
await client.components.create({ path: { space_id }, body: { component: {...} } });
96+
97+
// Spaces
98+
await client.spaces.list({});
99+
await client.spaces.get({ path: { space_id } });
100+
101+
// Datasource Entries
102+
await client.datasourceEntries.list({ path: { space_id, datasource_id } });
103+
104+
// Internal Tags
105+
await client.internalTags.list({ path: { space_id } });
106+
```
107+
108+
## Region Resolution
109+
110+
The client automatically determines the correct regional endpoint based on your space ID:
111+
112+
```typescript
113+
// These will automatically route to the correct regional endpoints:
114+
await client.stories.list({ path: { space_id: 564469716905585 } });
115+
// → https://api-us.storyblok.com
116+
117+
await client.stories.list({ path: { space_id: 845944693616241 } });
118+
// → https://api-ca.storyblok.com
119+
120+
await client.stories.list({ path: { space_id: 1127419670326897 } });
121+
// → https://api-ap.storyblok.com
122+
```
123+
124+
### Override Region Resolution
125+
```typescript
126+
const client = new ManagementApiClient({
127+
token: { accessToken: 'your-token' },
128+
baseUrl: 'https://custom-api.example.com' // Bypasses automatic region detection
129+
});
130+
```
131+
132+
## Retry Logic
133+
134+
The client includes built-in retry handling for rate limits and network errors:
135+
136+
- **429 Retry Handling**: Automatically retries on rate limit responses
137+
- **Retry-After Support**: Respects `retry-after` headers from the API
138+
- **Exponential Backoff**: Smart retry delays to avoid overwhelming the API
139+
- **Network Error Retry**: Retries on network failures
140+
141+
```typescript
142+
// The client automatically handles retries with these defaults:
143+
// - maxRetries: 3
144+
// - retryDelay: 1000ms
145+
// - Respects retry-after headers from 429 responses
146+
147+
const stories = await client.stories.list({
148+
path: { space_id: 123456 },
149+
query: { per_page: 10 }
150+
});
151+
// If rate limited, will automatically retry up to 3 times
152+
```
153+
154+
## Runtime Configuration
155+
156+
Update client configuration at runtime:
157+
158+
```typescript
159+
// Update region/baseUrl
160+
client.setConfig({
161+
region: 'us',
162+
headers: { 'Custom-Header': 'value' }
163+
});
164+
165+
// Update authentication
166+
client.setToken({ accessToken: 'new-token' });
167+
```
168+
169+
## Error Handling
170+
171+
```typescript
172+
try {
173+
const story = await client.stories.get({
174+
path: { space_id: 123456, story_id: 'non-existent' }
175+
});
176+
} catch (error) {
177+
if (error.status === 404) {
178+
console.log('Story not found');
179+
}
180+
}
181+
182+
// Or configure to not throw errors
183+
const client = new ManagementApiClient({
184+
token: { accessToken: 'your-token' },
185+
throwOnError: false
186+
});
187+
188+
const result = await client.stories.get({
189+
path: { space_id: 123456, story_id: 'non-existent' }
190+
});
191+
192+
if (result.error) {
193+
console.log('Error:', result.error);
194+
} else {
195+
console.log('Story:', result.data);
196+
}
197+
```
198+
199+
## TypeScript Support
200+
201+
The client provides full TypeScript support with generated types:
202+
203+
```typescript
204+
import type {
205+
StoriesTypes,
206+
DatasourcesTypes,
207+
ComponentsTypes
208+
} from '@storyblok/management-api-client';
209+
210+
// Full type safety for request/response data
211+
const createStory = async (storyData: StoriesTypes.CreateRequestBody) => {
212+
const response = await client.stories.create({
213+
path: { space_id: 123456 },
214+
body: storyData
215+
});
216+
217+
// Response is fully typed
218+
return response.data; // StoriesTypes.Story
219+
};
220+
```
221+
222+
## Development
223+
224+
```bash
225+
# Generate SDKs from OpenAPI specs
226+
pnpm generate
227+
228+
# Build the package
229+
pnpm build
230+
231+
# Run tests
232+
pnpm test
233+
```
234+
235+
## Contributing
236+
237+
This package is generated from OpenAPI specifications in `packages/openapi/`. To add new endpoints or modify existing ones, update the OpenAPI specs and regenerate the client.
238+
239+
## License
240+
241+
MIT
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { ManagementApiClient } from '../dist/index.mjs';
2+
3+
const client = new ManagementApiClient({
4+
token: {
5+
accessToken: process.env.MAPI_TOKEN!,
6+
},
7+
});
8+
9+
(async () => {
10+
const {data} = await client.spaces.list()
11+
12+
if (!data) {
13+
throw new Error('No spaces found')
14+
}
15+
16+
const space = data.spaces?.[0]
17+
if (!space) {
18+
throw new Error('No space found')
19+
}
20+
21+
const stories = await client.stories.list({
22+
path: {
23+
space_id: space.id,
24+
},
25+
query: {
26+
page: 1,
27+
per_page: 3
28+
},
29+
})
30+
31+
console.log(stories.data?.stories)
32+
})()

0 commit comments

Comments
 (0)