Source
Package Location
Directorymodules
Directorygoget
- index.ts
npm Dependencies
npm install server-only async-retry gqlmin radashyarn add server-only async-retry gqlmin radashpnpm add server-only async-retry gqlmin radashbun add server-only async-retry gqlmin radashserver-only: This package just ensures non of the code reaches the client as this client is primarily used to fetch data on the server. You can remove it if that isn’t a concern.async-retry: This handles the automatic retry functionality, it is fairly crucial to the package.gqlmin: This will minify the GraphQLquery, this is mainly used for APIs that include a byte limit on queries. For example; Contentful and ContentStack. This can be easily removed if you want to.radash: Using a few utilities from this package. This is usually a consistent dependency I include. You can easily replace it with something else or plain old JS if you wanted to.
Source Code
import "server-only";
import retry from "async-retry";import gqlmin from "gqlmin";import { get, isArray, isEmpty } from "radash";
type _Variables = Record<string, any>;type _Headers = Record<string, string>;
type GoGetConfig = { enableQueryDebug: boolean; defaultHeaders: _Headers; maxRequestByteSize: number; totalRetries: number;};
type Logger = { info: ( message: string, args?: { [key: string]: any; } ) => void;};
export type RequestInit = globalThis.RequestInit;
export interface QueryStrategy { alterInit?: (init: RequestInit) => RequestInit; postResponse?: (response: Response) => Promise<void>;}
export type QueryInput<Variables = _Variables> = { query: string; variables?: Variables; /** Additional headers, will override the base headers */ headers?: _Headers; /** * Utilise a strategy to modify the `init` object and/or react to response data. * * This will be where you and configure the cache strategy for your request. */ strategy?: QueryStrategy;};
export class GoGet { #uri: string;
#config: GoGetConfig = { enableQueryDebug: false, defaultHeaders: {}, maxRequestByteSize: 8192, totalRetries: 5, };
#logger: Logger = { info: console.log, };
constructor(uri: string, config?: Partial<GoGetConfig>) { this.#uri = uri; this.#config = Object.assign(this.#config, config); }
public setLogger(value: Logger) { this.#logger = value; return this; }
public gimme() { return this.query; }
private async query<Response = unknown, Variables = _Variables>( input: QueryInput<Variables> ): Promise<{ data: Response | null; error: Error | null; rawJson: unknown | null; }> { const { query, variables, strategy } = input;
const headers: Record<string, string> = { "Content-Type": "application/json", Accept: "application/json", ...this.#config.defaultHeaders, ...(input.headers || {}), };
const body = JSON.stringify({ query: gqlmin(query), ...(isEmpty(variables) ? {} : { variables }), });
let init: RequestInit = { method: "POST", headers, body, };
const regex = /\bquery\s+(\w+)/i; const match = query.match(regex); const queryName = match ? match[1] : "Unknown";
if (this.#config.enableQueryDebug) { const bytes = Buffer.byteLength(body, "utf8"); const percentage = (bytes / this.#config.maxRequestByteSize) * 100; const msg = `[${queryName}] Query size: (${bytes}/${this.#config.maxRequestByteSize}) bytes (${percentage.toFixed( 2 )}%)]`;
if (percentage > 80) { this.#logger?.info(`\x1b[31m${msg}\x1b[0m`); } else if (percentage > 50) { this.#logger?.info(`\x1b[33m${msg}\x1b[0m`); } else { this.#logger?.info(`\x1b[32m${msg}\x1b[0m`); } }
if (strategy?.alterInit) { init = strategy.alterInit(init); }
const result = await retry( async (_, attempt) => { const result = await fetch(this.#uri, init);
if (!result.ok) { switch (result.status) { case 429: this.#logger?.info("Too many requests, backing off", { target: queryName, attempt, }); throw new GoGetRateLimitException( "Too many requests, backing off" ); default: return { data: null, rawJson: null, error: new GoGetRequestException( `${result.status} ${result.statusText}: ${JSON.stringify( await result.json() )}` ), }; } }
if (strategy?.postResponse) { await strategy.postResponse(result); }
const json = await result.json();
const jsonData = get(json, "data"); const jsonErrors = get(json, "errors");
if (!isEmpty(jsonErrors) && isArray(jsonErrors)) { return { data: jsonData as Response, rawJson: json, error: new GoGetQueryException(jsonErrors), }; }
return { data: jsonData as Response, rawJson: json, error: null, }; }, { retries: this.#config.totalRetries, } );
return result; }}
export class GoGetRateLimitException extends Error {}
export class GoGetRequestException extends Error {}
export class GoGetQueryException extends Error { constructor(private errors: any[]) { super(); }
override get message() { console.error("[GoGetQueryException]", { errors: this.errors }); const errorMessages = this.errors .map((error) => error.message ?? undefined) .filter(Boolean) .join(". ") .trim();
return `API returned ${this.errors.length} errors. ${errorMessages}`; }}