Skip to content

Commit

Permalink
feat: new graphql method on the client, wrapping graphql calls for th…
Browse files Browse the repository at this point in the history
…e gapi endpoint
  • Loading branch information
edodusi committed Nov 15, 2024
1 parent 8bdd0ab commit 26b76ba
Show file tree
Hide file tree
Showing 11 changed files with 302 additions and 87 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@
"vite": "^5.4.11",
"vite-plugin-banner": "^0.8.0",
"vite-plugin-dts": "^4.3.0",
"vitest": "^2.1.4"
"vitest": "^2.1.4",
"vitest-fetch-mock": "^0.4.2"
},
"release": {
"branches": [
Expand Down
16 changes: 16 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ export const STORYBLOK_JS_CLIENT_AGENT = {
defaultAgentVersion: 'SB-Agent-Version',
packageVersion: '6.0.0',
};

export const STORYBLOK_GRAPQL_API = 'https://gapi.storyblok.com/v1/api';
78 changes: 78 additions & 0 deletions src/graphql-wrapper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import createFetchMock from 'vitest-fetch-mock';

describe('test graphql wrapper', () => {
const query = `
query {
PageItem(id: "home") {
name
content {
_uid
component
}
}
}
`;

const accessToken = 'test-access-token';
const version = 'draft';
const variables = { id: '123' };

beforeAll(() => {
const fetchMocker = createFetchMock(vi);
// sets globalThis.fetch and globalThis.fetchMock to our mocked version
fetchMocker.enableMocks();
});

beforeEach(() => {
fetch.resetMocks();
});

it('should return data when the request is successful', async () => {
fetch.mockResponseOnce(JSON.stringify({ data: { test: 'test' } }));

const { graph } = await import('./graphql-wrapper');
const response = await graph(query, accessToken, version, variables);

expect(response).toEqual({ data: { test: 'test' } });
});

it('should throw an error when the request fails', async () => {
fetch.mockRejectOnce(new Error('test error'));

const { graph } = await import('./graphql-wrapper');

try {
await graph(query, accessToken, version, variables);
}
catch (error) {
expect(error.message).toBe('GraphQL request failed: test error');
}
});

it('should throw an error when the response status is not ok', async () => {
fetch.mockResponseOnce(JSON.stringify({ data: { test: 'test' } }), { status: 401 });

const { graph } = await import('./graphql-wrapper');

try {
await graph(query, accessToken, version, variables);
}
catch (error) {
expect(error.message).toBe('GraphQL request failed with status 401');
}
});

it('should throw an error when the response is not JSON', async () => {
fetch.mockResponseOnce('not json', { status: 200 });

const { graph } = await import('./graphql-wrapper');

try {
await graph(query, accessToken, version, variables);
}
catch (error) {
expect(error.message).toContain('Unexpected token');
}
});
});
41 changes: 41 additions & 0 deletions src/graphql-wrapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { STORYBLOK_GRAPQL_API } from './constants';

/**
* Wrapper for Storyblok GraphQL API
*
* @param query string
* @param accessToken string
* @param version 'draft' | 'published'
* @param variables Record<string, unknown>
* @returns Promise<{ data: object }>
*
* @throws Error
*/
export async function graph(
query: string,
accessToken: string,
version: 'draft' | 'published' = 'draft',
variables?: Record<string, unknown>,
): Promise<{ data: object }> {
let response;
try {
response = await fetch(STORYBLOK_GRAPQL_API, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'token': accessToken,
'version': version,
},
body: JSON.stringify({ query, variables }),
});
}
catch (error) {
throw new Error(`GraphQL request failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}

if (!response.ok) {
throw new Error(`GraphQL request failed with status ${response.status}`);
}

return response.json();
}
13 changes: 12 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import type {
ISbStoryParams,
ThrottleFn,
} from './interfaces';
import type { IStoryblok } from './storyblok';
import { graph } from './graphql-wrapper';

let memory: Partial<IMemoryType> = {};

Expand Down Expand Up @@ -64,7 +66,7 @@ const _VERSION = {
type ObjectValues<T> = T[keyof T];
type Version = ObjectValues<typeof _VERSION>;

class Storyblok {
class Storyblok implements IStoryblok {
private client: SbFetch;
private maxRetries: number;
private retriesDelay: number;
Expand Down Expand Up @@ -751,6 +753,15 @@ class Storyblok {
this.clearCacheVersion();
return this;
}

// Wrap GraphQL queries
public async graphql(
query: string,
version: 'draft' | 'published' = 'draft',
variables?: Record<string, unknown>,
): Promise<{ data: object }> {
return graph(query, this.accessToken, version, variables);
}
}

export default Storyblok;
67 changes: 67 additions & 0 deletions src/storyblok.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import type {
CachedVersions,
ComponentResolverFn,
ISbContentMangmntAPI,
ISbCustomFetch,
ISbResponseData,
ISbResult,
ISbStories,
ISbStoriesParams,
ISbStory,
ISbStoryParams,
LinksType,
RelationsType,
RichTextResolver,
} from './interfaces';

export interface IStoryblok {
relations: RelationsType;
links: LinksType;
richTextResolver: RichTextResolver;
resolveNestedRelations: boolean;

// Sets the component resolver for rich text
setComponentResolver: (resolver: ComponentResolverFn) => void;

// Fetches a single story by slug
get: (slug: string, params?: ISbStoriesParams, fetchOptions?: ISbCustomFetch) => Promise<ISbResult>;

// Fetches all stories matching the given parameters
getAll: (slug: string, params: ISbStoriesParams, entity?: string, fetchOptions?: ISbCustomFetch) => Promise<any[]>;

// Creates a new story
post: (slug: string, params: ISbStoriesParams | ISbContentMangmntAPI, fetchOptions?: ISbCustomFetch) => Promise<ISbResponseData>;

// Updates an existing story
put: (slug: string, params: ISbStoriesParams | ISbContentMangmntAPI, fetchOptions?: ISbCustomFetch) => Promise<ISbResponseData>;

// Deletes a story
delete: (slug: string, params: ISbStoriesParams | ISbContentMangmntAPI, fetchOptions?: ISbCustomFetch) => Promise<ISbResponseData>;

// Fetches multiple stories
getStories: (params: ISbStoriesParams, fetchOptions?: ISbCustomFetch) => Promise<ISbStories>;

// Fetches a single story by slug
getStory: (slug: string, params: ISbStoryParams, fetchOptions?: ISbCustomFetch) => Promise<ISbStory>;

// Wrapper for GraphQL queries
graphql: (query: string, version: 'draft' | 'published', variables?: Record<string, unknown>) => Promise<{ data: object }>;

// Ejects the interceptor from the fetch client
ejectInterceptor: () => void;

// Flushes all caches
flushCache: () => Promise<this>;

// Returns all cached versions (cv)
cacheVersions: () => CachedVersions;

// Returns the current cache version (cv)
cacheVersion: () => number;

// Sets the cache version (cv)
setCacheVersion: (cv: number) => void;

// Clears the cache version
clearCacheVersion: () => void;
}
Loading

0 comments on commit 26b76ba

Please sign in to comment.