All files / src/graphql client.ts

96.66% Statements 29/30
100% Branches 12/12
90% Functions 9/10
96.42% Lines 27/28

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109255x   255x 255x   255x 255x             255x     73x     73x 73x                 2x       28x                                         26x 26x 26x   27x         26x             26x                 25x 1x       24x         22x 3x     20x 1x     19x       6x       3x      
import { ExecutionResult, print } from 'graphql';
import { TypedDocumentNode } from '@graphql-typed-document-node/core';
import { DEFAULT_HEADERS } from '../http-client';
import { enhancedFetch } from '../utils/fetch';
import type { SchemaFieldIndex } from '../services/SchemaIntrospector';
import { prepareDocument } from './prepare-document';
import { readBoundedJson } from '../utils/bounded-json';
 
export interface GraphQLClientOptions {
  endpoint: string;
  headers?: Record<string, string>;
}
 
export class GraphQLClient {
  private _endpoint: string;
  private defaultHeaders: Record<string, string>;
  private schemaIndexProvider: () => SchemaFieldIndex | undefined = () => undefined;
 
  constructor(endpoint: string, options?: { headers?: Record<string, string> }) {
    this._endpoint = endpoint;
    this.defaultHeaders = options?.headers ?? {};
  }
 
  /**
   * Source of the instance schema used to adapt each document before sending it
   * (see prepareDocument). Read per request, so it sees introspection results
   * that arrive after the client was created.
   */
  public setSchemaIndexProvider(provider: () => SchemaFieldIndex | undefined): void {
    this.schemaIndexProvider = provider;
  }
 
  public get endpoint(): string {
    return this._endpoint;
  }
 
  /**
   * Update the GraphQL endpoint.
   *
   * @deprecated Since multi-instance support (v1.x), prefer using per-instance
   * clients via InstanceConnectionPool.getGraphQLClient() instead of mutating
   * a shared client. This avoids race conditions in concurrent async scenarios.
   * Kept for backward compatibility with legacy single-instance usage.
   */
  public setEndpoint(endpoint: string): void {
    this._endpoint = endpoint;
  }
 
  async request<TResult = unknown, TVariables = Record<string, unknown>>(
    document: TypedDocumentNode<TResult, TVariables>,
    variables?: TVariables,
    requestHeaders?: Record<string, string>,
    responseByteLimit?: number,
  ): Promise<TResult> {
    const prepared = prepareDocument(document, this.schemaIndexProvider());
    const query = print(prepared.document);
    const sentVariables = Object.fromEntries(
      Object.entries((variables ?? {}) as Record<string, unknown>).filter(([name]) =>
        prepared.variableNames.has(name),
      ),
    );
 
    // Prepare headers with authentication (enhancedFetch handles cookies automatically)
    const headers: Record<string, string> = {
      'Content-Type': 'application/json',
      ...DEFAULT_HEADERS,
      ...this.defaultHeaders,
      ...requestHeaders,
    };
 
    const response = await enhancedFetch(this.endpoint, {
      method: 'POST',
      headers,
      body: JSON.stringify({
        query,
        variables: sentVariables,
      }),
    });
 
    if (!response.ok) {
      throw new Error(`GraphQL request failed: ${response.status} ${response.statusText}`);
    }
 
    const result = (
      responseByteLimit === undefined
        ? await response.json()
        : await readBoundedJson(response, responseByteLimit)
    ) as ExecutionResult<TResult>;
 
    if (result.errors) {
      throw new Error(`GraphQL errors: ${result.errors.map((e) => e.message).join(', ')}`);
    }
 
    if (!result.data) {
      throw new Error('GraphQL request returned no data');
    }
 
    return result.data;
  }
 
  setHeaders(headers: Record<string, string>): void {
    this.defaultHeaders = { ...this.defaultHeaders, ...headers };
  }
 
  setAuthToken(token: string): void {
    this.setHeaders({ Authorization: `Bearer ${token}` });
  }
}