diff options
Diffstat (limited to 'source/library/fetcher/apq.ts')
| -rw-r--r-- | source/library/fetcher/apq.ts | 101 |
1 files changed, 101 insertions, 0 deletions
diff --git a/source/library/fetcher/apq.ts b/source/library/fetcher/apq.ts new file mode 100644 index 0000000..7f9f3c1 --- /dev/null +++ b/source/library/fetcher/apq.ts @@ -0,0 +1,101 @@ + + + +/*** UTILITY ------------------------------------------ ***/ + +import { buildHeaders, postJson } from "./http-body.ts"; +import type { Fetcher, FetcherOptions } from "./types.ts"; + +/*** EXPORT ------------------------------------------- ***/ + +export type ApqOptions = FetcherOptions & { + disable?: boolean; +}; + +export function createApqFetcher(options: ApqOptions): Fetcher { + const cache = new Map<string, string>(); + const fetchImpl = options.fetch ?? globalThis.fetch; + + return async (req) => { + const headers = buildHeaders(options, req); + const sha256Hash = await getHash(cache, req.query); + + const extensions = { + persistedQuery: { + sha256Hash, + version: 1 + } + }; + + if (options.disable === true) { + return await postJson(fetchImpl, options.url, headers, { + extensions, + operationName: req.operationName, + query: req.query, + variables: req.variables + }); + } + + const firstResponse = await postJson(fetchImpl, options.url, headers, { + extensions, + operationName: req.operationName, + variables: req.variables + }); + + if (!isPersistedQueryNotFound(firstResponse)) + return firstResponse; + + return await postJson(fetchImpl, options.url, headers, { + extensions, + operationName: req.operationName, + query: req.query, + variables: req.variables + }); + }; +} + +/*** INTERNAL ----------------------------------------- ***/ + +async function getHash(cache: Map<string, string>, query: string): Promise<string> { + const cached = cache.get(query); + + if (cached !== undefined) + return cached; + + const hash = await sha256Hex(query); + cache.set(query, hash); + return hash; +} + +function isPersistedQueryNotFound(response: Record<string, unknown>): boolean { + const errors = response.errors; + + if (!Array.isArray(errors)) + return false; + + for (const entry of errors) { + if (entry === null || typeof entry !== "object") + continue; + + const err = entry as Record<string, unknown>; + + if (err.message === "PersistedQueryNotFound") + return true; + + const ext = err.extensions; + + if (ext !== null && typeof ext === "object") { + const code = (ext as Record<string, unknown>).code; + + if (code === "PERSISTED_QUERY_NOT_FOUND") + return true; + } + } + + return false; +} + +async function sha256Hex(input: string): Promise<string> { + const buf = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(input)); + return Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join(""); +} |