Skip to content
This repository was archived by the owner on Jun 24, 2025. It is now read-only.

Commit db648b0

Browse files
committed
feat: new graphql method on the client, wrapping graphql calls for the gapi endpoint
1 parent 8bdd0ab commit db648b0

9 files changed

Lines changed: 221 additions & 5 deletions

File tree

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@
7676
"vite": "^5.4.11",
7777
"vite-plugin-banner": "^0.8.0",
7878
"vite-plugin-dts": "^4.3.0",
79-
"vitest": "^2.1.4"
79+
"vitest": "^2.1.4",
80+
"vitest-fetch-mock": "^0.4.2"
8081
},
8182
"release": {
8283
"branches": [

pnpm-lock.yaml

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,5 @@ export const STORYBLOK_JS_CLIENT_AGENT = {
1717
defaultAgentVersion: 'SB-Agent-Version',
1818
packageVersion: '6.0.0',
1919
};
20+
21+
export const STORYBLOK_GRAPQL_API = 'https://gapi.storyblok.com/v1/api';

src/graphql-wrapper.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { beforeEach, describe, expect, it } from 'vitest';
2+
3+
describe('test graphql wrapper', () => {
4+
const query = `
5+
query {
6+
PageItem(id: "home") {
7+
name
8+
content {
9+
_uid
10+
component
11+
}
12+
}
13+
}
14+
`;
15+
16+
const accessToken = 'test-access-token';
17+
const version = 'draft';
18+
const variables = { id: '123' };
19+
20+
beforeEach(() => {
21+
fetch.resetMocks();
22+
});
23+
24+
it('should return data when the request is successful', async () => {
25+
fetch.mockResponseOnce(JSON.stringify({ data: { test: 'test' } }));
26+
27+
const { graph } = await import('./graphql-wrapper');
28+
const response = await graph(query, accessToken, version, variables);
29+
30+
expect(response).toEqual({ data: { test: 'test' } });
31+
});
32+
33+
it('should throw an error when the request fails', async () => {
34+
fetch.mockRejectOnce(new Error('test error'));
35+
36+
const { graph } = await import('./graphql-wrapper');
37+
38+
try {
39+
await graph(query, accessToken, version, variables);
40+
}
41+
catch (error) {
42+
expect(error.message).toBe('GraphQL request failed: test error');
43+
}
44+
});
45+
46+
it('should throw an error when the response status is not ok', async () => {
47+
fetch.mockResponseOnce(JSON.stringify({ data: { test: 'test' } }), { status: 401 });
48+
49+
const { graph } = await import('./graphql-wrapper');
50+
51+
try {
52+
await graph(query, accessToken, version, variables);
53+
}
54+
catch (error) {
55+
expect(error.message).toBe('GraphQL request failed with status 401');
56+
}
57+
});
58+
59+
it('should throw an error when the response is not JSON', async () => {
60+
fetch.mockResponseOnce('not json', { status: 200 });
61+
62+
const { graph } = await import('./graphql-wrapper');
63+
64+
try {
65+
await graph(query, accessToken, version, variables);
66+
}
67+
catch (error) {
68+
expect(error.message).toContain('Unexpected token');
69+
}
70+
});
71+
});

src/graphql-wrapper.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { STORYBLOK_GRAPQL_API } from './constants';
2+
3+
/**
4+
* Wrapper for Storyblok GraphQL API
5+
*
6+
* @param query string
7+
* @param accessToken string
8+
* @param version 'draft' | 'published'
9+
* @param variables Record<string, unknown>
10+
* @returns Promise<{ data: object }>
11+
*
12+
* @throws Error
13+
*/
14+
export async function graph(
15+
query: string,
16+
accessToken: string,
17+
version: 'draft' | 'published' = 'draft',
18+
variables?: Record<string, unknown>,
19+
): Promise<{ data: object }> {
20+
let response;
21+
try {
22+
response = await fetch(STORYBLOK_GRAPQL_API, {
23+
method: 'POST',
24+
headers: {
25+
'Content-Type': 'application/json',
26+
'token': accessToken,
27+
'version': version,
28+
},
29+
body: JSON.stringify({ query, variables }),
30+
});
31+
}
32+
catch (error) {
33+
throw new Error(`GraphQL request failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
34+
}
35+
36+
if (!response.ok) {
37+
throw new Error(`GraphQL request failed with status ${response.status}`);
38+
}
39+
40+
return response.json();
41+
}

src/index.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import type {
2222
ISbStoryParams,
2323
ThrottleFn,
2424
} from './interfaces';
25+
import type { IStoryblok } from './storyblok';
26+
import { graph } from './graphql-wrapper';
2527

2628
let memory: Partial<IMemoryType> = {};
2729

@@ -64,7 +66,7 @@ const _VERSION = {
6466
type ObjectValues<T> = T[keyof T];
6567
type Version = ObjectValues<typeof _VERSION>;
6668

67-
class Storyblok {
69+
class Storyblok implements IStoryblok {
6870
private client: SbFetch;
6971
private maxRetries: number;
7072
private retriesDelay: number;
@@ -751,6 +753,15 @@ class Storyblok {
751753
this.clearCacheVersion();
752754
return this;
753755
}
756+
757+
// Wrap GraphQL queries
758+
public async graphql(
759+
query: string,
760+
version: 'draft' | 'published' = 'draft',
761+
variables?: Record<string, unknown>,
762+
): Promise<{ data: object }> {
763+
return graph(query, this.accessToken, version, variables);
764+
}
754765
}
755766

756767
export default Storyblok;

src/storyblok.d.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import type {
2+
CachedVersions,
3+
ComponentResolverFn,
4+
ISbContentMangmntAPI,
5+
ISbCustomFetch,
6+
ISbResponseData,
7+
ISbResult,
8+
ISbStories,
9+
ISbStoriesParams,
10+
ISbStory,
11+
ISbStoryParams,
12+
LinksType,
13+
RelationsType,
14+
RichTextResolver,
15+
} from './interfaces';
16+
17+
export interface IStoryblok {
18+
relations: RelationsType;
19+
links: LinksType;
20+
richTextResolver: RichTextResolver;
21+
resolveNestedRelations: boolean;
22+
23+
// Sets the component resolver for rich text
24+
setComponentResolver: (resolver: ComponentResolverFn) => void;
25+
26+
// Fetches a single story by slug
27+
get: (slug: string, params?: ISbStoriesParams, fetchOptions?: ISbCustomFetch) => Promise<ISbResult>;
28+
29+
// Fetches all stories matching the given parameters
30+
getAll: (slug: string, params: ISbStoriesParams, entity?: string, fetchOptions?: ISbCustomFetch) => Promise<any[]>;
31+
32+
// Creates a new story
33+
post: (slug: string, params: ISbStoriesParams | ISbContentMangmntAPI, fetchOptions?: ISbCustomFetch) => Promise<ISbResponseData>;
34+
35+
// Updates an existing story
36+
put: (slug: string, params: ISbStoriesParams | ISbContentMangmntAPI, fetchOptions?: ISbCustomFetch) => Promise<ISbResponseData>;
37+
38+
// Deletes a story
39+
delete: (slug: string, params: ISbStoriesParams | ISbContentMangmntAPI, fetchOptions?: ISbCustomFetch) => Promise<ISbResponseData>;
40+
41+
// Fetches multiple stories
42+
getStories: (params: ISbStoriesParams, fetchOptions?: ISbCustomFetch) => Promise<ISbStories>;
43+
44+
// Fetches a single story by slug
45+
getStory: (slug: string, params: ISbStoryParams, fetchOptions?: ISbCustomFetch) => Promise<ISbStory>;
46+
47+
// Wrapper for GraphQL queries
48+
graphql: (query: string, version: 'draft' | 'published', variables?: Record<string, unknown>) => Promise<any>;
49+
50+
// Ejects the interceptor from the fetch client
51+
ejectInterceptor: () => void;
52+
53+
// Flushes all caches
54+
flushCache: () => Promise<this>;
55+
56+
// Returns all cached versions (cv)
57+
cacheVersions: () => CachedVersions;
58+
59+
// Returns the current cache version (cv)
60+
cacheVersion: () => number;
61+
62+
// Sets the cache version (cv)
63+
setCacheVersion: (cv: number) => void;
64+
65+
// Clears the cache version
66+
clearCacheVersion: () => void;
67+
}

tests/setup.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,8 @@
1-
import 'isomorphic-fetch'
1+
import 'isomorphic-fetch';
2+
import createFetchMock from 'vitest-fetch-mock';
3+
import { vi } from 'vitest';
4+
5+
const fetchMocker = createFetchMock(vi);
6+
7+
// sets globalThis.fetch and globalThis.fetchMock to our mocked version
8+
fetchMocker.enableMocks();

vitest.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { defineConfig } from 'vite'
1+
import { defineConfig } from 'vite';
22

33
export default defineConfig({
44
test: {
@@ -10,4 +10,4 @@ export default defineConfig({
1010
reportsDirectory: './tests/unit/coverage',
1111
},
1212
},
13-
})
13+
});

0 commit comments

Comments
 (0)