File

src/util.ts

Extends

Omit

Index

Properties

Properties

authClient
authClient: AuthClient | GoogleAuth
Type : AuthClient | GoogleAuth
Optional

A pre-instantiated AuthClient or GoogleAuth client that should be used. A new will be created if this is not set.

autoRetry
autoRetry: boolean
Type : boolean
Optional

Automatically retry requests if the response is related to rate limits or certain intermittent server errors. We will exponentially backoff subsequent requests by default. (default: true)

customEndpoint
customEndpoint: boolean
Type : boolean
Optional

If true, just return the provided request options. Default: false.

email
email: string
Type : string
Optional

Account email address, required for PEM/P12 usage.

maxRetries
maxRetries: number
Type : number
Optional

Maximum number of automatic retries attempted before returning the error. (default: 3)

projectIdRequired
projectIdRequired: boolean
Type : boolean
Optional

Determines if a projectId is required for authenticated requests. Defaults to true.

stream
stream: Duplexify
Type : Duplexify
Optional
useAuthWithCustomEndpoint
useAuthWithCustomEndpoint: boolean
Type : boolean
Optional

If true, will authenticate when using a custom endpoint. Default: false.

import {
  replaceProjectIdToken,
  MissingProjectIdError,
} from '@google-cloud/projectify';
import * as ent from 'ent';
import * as extend from 'extend';
import {AuthClient, GoogleAuth, GoogleAuthOptions} from 'google-auth-library';
import {CredentialBody} from 'google-auth-library';
import * as r from 'teeny-request';
import * as retryRequest from 'retry-request';
import {Duplex, DuplexOptions, Readable, Transform, Writable} from 'stream';
import {teenyRequest} from 'teeny-request';

import {Interceptor} from './service-object';
import {DEFAULT_PROJECT_ID_TOKEN} from './service';

// eslint-disable-next-line @typescript-eslint/no-var-requires
const duplexify: DuplexifyConstructor = require('duplexify');

const requestDefaults = {
  timeout: 60000,
  gzip: true,
  forever: true,
  pool: {
    maxSockets: Infinity,
  },
};

/**
 * Default behavior: Automatically retry retriable server errors.
 *
 * @const {boolean}
 * @private
 */
const AUTO_RETRY_DEFAULT = true;

/**
 * Default behavior: Only attempt to retry retriable errors 3 times.
 *
 * @const {number}
 * @private
 */
const MAX_RETRY_DEFAULT = 3;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ResponseBody = any;

// Directly copy over Duplexify interfaces
export interface DuplexifyOptions extends DuplexOptions {
  autoDestroy?: boolean;
  end?: boolean;
}

export interface Duplexify extends Duplex {
  readonly destroyed: boolean;
  setWritable(writable: Writable | false | null): void;
  setReadable(readable: Readable | false | null): void;
}

export interface DuplexifyConstructor {
  obj(
    writable?: Writable | false | null,
    readable?: Readable | false | null,
    options?: DuplexifyOptions
  ): Duplexify;
  new (
    writable?: Writable | false | null,
    readable?: Readable | false | null,
    options?: DuplexifyOptions
  ): Duplexify;
  (
    writable?: Writable | false | null,
    readable?: Readable | false | null,
    options?: DuplexifyOptions
  ): Duplexify;
}

export interface ParsedHttpRespMessage {
  resp: r.Response;
  err?: ApiError;
}

export interface MakeAuthenticatedRequest {
  (reqOpts: DecorateRequestOptions): Duplexify;
  (
    reqOpts: DecorateRequestOptions,
    options?: MakeAuthenticatedRequestOptions
  ): void | Abortable;
  (
    reqOpts: DecorateRequestOptions,
    callback?: BodyResponseCallback
  ): void | Abortable;
  (
    reqOpts: DecorateRequestOptions,
    optionsOrCallback?: MakeAuthenticatedRequestOptions | BodyResponseCallback
  ): void | Abortable | Duplexify;
  getCredentials: (
    callback: (err?: Error | null, credentials?: CredentialBody) => void
  ) => void;
  authClient: GoogleAuth<AuthClient>;
}

export interface Abortable {
  abort(): void;
}
export type AbortableDuplex = Duplexify & Abortable;

export interface PackageJson {
  name: string;
  version: string;
}

export interface MakeAuthenticatedRequestFactoryConfig
  extends Omit<GoogleAuthOptions, 'authClient'> {
  /**
   * Automatically retry requests if the response is related to rate limits or
   * certain intermittent server errors. We will exponentially backoff
   * subsequent requests by default. (default: true)
   */
  autoRetry?: boolean;

  /**
   * If true, just return the provided request options. Default: false.
   */
  customEndpoint?: boolean;

  /**
   * If true, will authenticate when using a custom endpoint. Default: false.
   */
  useAuthWithCustomEndpoint?: boolean;

  /**
   * Account email address, required for PEM/P12 usage.
   */
  email?: string;

  /**
   * Maximum number of automatic retries attempted before returning the error.
   * (default: 3)
   */
  maxRetries?: number;

  stream?: Duplexify;

  /**
   * A pre-instantiated `AuthClient` or `GoogleAuth` client that should be used.
   * A new will be created if this is not set.
   */
  authClient?: AuthClient | GoogleAuth;

  /**
   * Determines if a projectId is required for authenticated requests. Defaults to `true`.
   */
  projectIdRequired?: boolean;
}

export interface MakeAuthenticatedRequestOptions {
  onAuthenticated: OnAuthenticatedCallback;
}

export interface OnAuthenticatedCallback {
  (err: Error | null, reqOpts?: DecorateRequestOptions): void;
}

export interface GoogleErrorBody {
  code: number;
  errors?: GoogleInnerError[];
  response: r.Response;
  message?: string;
}

export interface GoogleInnerError {
  reason?: string;
  message?: string;
}

export interface MakeWritableStreamOptions {
  /**
   * A connection instance used to get a token with and send the request
   * through.
   */
  connection?: {};

  /**
   * Metadata to send at the head of the request.
   */
  metadata?: {contentType?: string};

  /**
   * Request object, in the format of a standard Node.js http.request() object.
   */
  request?: r.Options;

  makeAuthenticatedRequest(
    reqOpts: r.OptionsWithUri,
    fnobj: {
      onAuthenticated(
        err: Error | null,
        authenticatedReqOpts?: r.Options
      ): void;
    }
  ): void;
}

export interface DecorateRequestOptions extends r.CoreOptions {
  autoPaginate?: boolean;
  autoPaginateVal?: boolean;
  objectMode?: boolean;
  maxRetries?: number;
  uri: string;
  interceptors_?: Interceptor[];
  shouldReturnStream?: boolean;
  projectId?: string;
}

export interface ParsedHttpResponseBody {
  body: ResponseBody;
  err?: Error;
}

/**
 * Custom error type for API errors.
 *
 * @param {object} errorBody - Error object.
 */
export class ApiError extends Error {
  code?: number;
  errors?: GoogleInnerError[];
  response?: r.Response;
  constructor(errorMessage: string);
  constructor(errorBody: GoogleErrorBody);
  constructor(errorBodyOrMessage?: GoogleErrorBody | string) {
    super();
    if (typeof errorBodyOrMessage !== 'object') {
      this.message = errorBodyOrMessage || '';
      return;
    }
    const errorBody = errorBodyOrMessage;

    this.code = errorBody.code;
    this.errors = errorBody.errors;
    this.response = errorBody.response;

    try {
      this.errors = JSON.parse(this.response.body).error.errors;
    } catch (e) {
      this.errors = errorBody.errors;
    }

    this.message = ApiError.createMultiErrorMessage(errorBody, this.errors);
    Error.captureStackTrace(this);
  }
  /**
   * Pieces together an error message by combining all unique error messages
   * returned from a single GoogleError
   *
   * @private
   *
   * @param {GoogleErrorBody} err The original error.
   * @param {GoogleInnerError[]} [errors] Inner errors, if any.
   * @returns {string}
   */
  static createMultiErrorMessage(
    err: GoogleErrorBody,
    errors?: GoogleInnerError[]
  ): string {
    const messages: Set<string> = new Set();

    if (err.message) {
      messages.add(err.message);
    }

    if (errors && errors.length) {
      errors.forEach(({message}) => messages.add(message!));
    } else if (err.response && err.response.body) {
      messages.add(ent.decode(err.response.body.toString()));
    } else if (!err.message) {
      messages.add('A failure occurred during this request.');
    }

    let messageArr: string[] = Array.from(messages);

    if (messageArr.length > 1) {
      messageArr = messageArr.map((message, i) => `    ${i + 1}. ${message}`);
      messageArr.unshift(
        'Multiple errors occurred during the request. Please see the `errors` array for complete details.\n'
      );
      messageArr.push('\n');
    }

    return messageArr.join('\n');
  }
}

/**
 * Custom error type for partial errors returned from the API.
 *
 * @param {object} b - Error object.
 */
export class PartialFailureError extends Error {
  errors?: GoogleInnerError[];
  response?: r.Response;
  constructor(b: GoogleErrorBody) {
    super();
    const errorObject = b;

    this.errors = errorObject.errors;
    this.name = 'PartialFailureError';
    this.response = errorObject.response;

    this.message = ApiError.createMultiErrorMessage(errorObject, this.errors);
  }
}

export interface BodyResponseCallback {
  (err: Error | ApiError | null, body?: ResponseBody, res?: r.Response): void;
}

export interface RetryOptions {
  retryDelayMultiplier?: number;
  totalTimeout?: number;
  maxRetryDelay?: number;
  autoRetry?: boolean;
  maxRetries?: number;
  retryableErrorFn?: (err: ApiError) => boolean;
}

export interface MakeRequestConfig {
  /**
   * Automatically retry requests if the response is related to rate limits or
   * certain intermittent server errors. We will exponentially backoff
   * subsequent requests by default. (default: true)
   */
  autoRetry?: boolean;

  /**
   * Maximum number of automatic retries attempted before returning the error.
   * (default: 3)
   */
  maxRetries?: number;

  retries?: number;

  retryOptions?: RetryOptions;

  stream?: Duplexify;

  shouldRetryFn?: (response?: r.Response) => boolean;
}

export class Util {
  ApiError = ApiError;
  PartialFailureError = PartialFailureError;

  /**
   * No op.
   *
   * @example
   * function doSomething(callback) {
   *   callback = callback || noop;
   * }
   */
  noop() {}

  /**
   * Uniformly process an API response.
   *
   * @param {*} err - Error value.
   * @param {*} resp - Response value.
   * @param {*} body - Body value.
   * @param {function} callback - The callback function.
   */
  handleResp(
    err: Error | null,
    resp?: r.Response | null,
    body?: ResponseBody,
    callback?: BodyResponseCallback
  ) {
    callback = callback || util.noop;

    const parsedResp = extend(
      true,
      {err: err || null},
      resp && util.parseHttpRespMessage(resp),
      body && util.parseHttpRespBody(body)
    );
    // Assign the parsed body to resp.body, even if { json: false } was passed
    // as a request option.
    // We assume that nobody uses the previously unparsed value of resp.body.
    if (!parsedResp.err && resp && typeof parsedResp.body === 'object') {
      parsedResp.resp.body = parsedResp.body;
    }

    if (parsedResp.err && resp) {
      parsedResp.err.response = resp;
    }

    callback(parsedResp.err, parsedResp.body, parsedResp.resp);
  }

  /**
   * Sniff an incoming HTTP response message for errors.
   *
   * @param {object} httpRespMessage - An incoming HTTP response message from `request`.
   * @return {object} parsedHttpRespMessage - The parsed response.
   * @param {?error} parsedHttpRespMessage.err - An error detected.
   * @param {object} parsedHttpRespMessage.resp - The original response object.
   */
  parseHttpRespMessage(httpRespMessage: r.Response) {
    const parsedHttpRespMessage = {
      resp: httpRespMessage,
    } as ParsedHttpRespMessage;

    if (httpRespMessage.statusCode < 200 || httpRespMessage.statusCode > 299) {
      // Unknown error. Format according to ApiError standard.
      parsedHttpRespMessage.err = new ApiError({
        errors: new Array<GoogleInnerError>(),
        code: httpRespMessage.statusCode,
        message: httpRespMessage.statusMessage,
        response: httpRespMessage,
      });
    }

    return parsedHttpRespMessage;
  }

  /**
   * Parse the response body from an HTTP request.
   *
   * @param {object} body - The response body.
   * @return {object} parsedHttpRespMessage - The parsed response.
   * @param {?error} parsedHttpRespMessage.err - An error detected.
   * @param {object} parsedHttpRespMessage.body - The original body value provided
   *     will try to be JSON.parse'd. If it's successful, the parsed value will
   * be returned here, otherwise the original value and an error will be returned.
   */
  parseHttpRespBody(body: ResponseBody) {
    const parsedHttpRespBody: ParsedHttpResponseBody = {
      body,
    };

    if (typeof body === 'string') {
      try {
        parsedHttpRespBody.body = JSON.parse(body);
      } catch (err) {
        parsedHttpRespBody.body = body;
      }
    }

    if (parsedHttpRespBody.body && parsedHttpRespBody.body.error) {
      // Error from JSON API.
      parsedHttpRespBody.err = new ApiError(parsedHttpRespBody.body.error);
    }

    return parsedHttpRespBody;
  }

  /**
   * Take a Duplexify stream, fetch an authenticated connection header, and
   * create an outgoing writable stream.
   *
   * @param {Duplexify} dup - Duplexify stream.
   * @param {object} options - Configuration object.
   * @param {module:common/connection} options.connection - A connection instance used to get a token with and send the request through.
   * @param {object} options.metadata - Metadata to send at the head of the request.
   * @param {object} options.request - Request object, in the format of a standard Node.js http.request() object.
   * @param {string=} options.request.method - Default: "POST".
   * @param {string=} options.request.qs.uploadType - Default: "multipart".
   * @param {string=} options.streamContentType - Default: "application/octet-stream".
   * @param {function} onComplete - Callback, executed after the writable Request stream has completed.
   */
  makeWritableStream(
    dup: Duplexify,
    options: MakeWritableStreamOptions,
    onComplete?: Function
  ) {
    onComplete = onComplete || util.noop;

    const writeStream = new ProgressStream();
    writeStream.on('progress', evt => dup.emit('progress', evt));
    dup.setWritable(writeStream);

    const defaultReqOpts = {
      method: 'POST',
      qs: {
        uploadType: 'multipart',
      },
      timeout: 0,
      maxRetries: 0,
    };

    const metadata = options.metadata || {};

    const reqOpts = extend(true, defaultReqOpts, options.request, {
      multipart: [
        {
          'Content-Type': 'application/json',
          body: JSON.stringify(metadata),
        },
        {
          'Content-Type': metadata.contentType || 'application/octet-stream',
          body: writeStream,
        },
      ],
    }) as r.OptionsWithUri;

    options.makeAuthenticatedRequest(reqOpts, {
      onAuthenticated(err, authenticatedReqOpts) {
        if (err) {
          dup.destroy(err);
          return;
        }

        const request = teenyRequest.defaults(requestDefaults);
        request(authenticatedReqOpts!, (err, resp, body) => {
          util.handleResp(err, resp, body, (err, data) => {
            if (err) {
              dup.destroy(err);
              return;
            }
            dup.emit('response', resp);
            onComplete!(data);
          });
        });
      },
    });
  }

  /**
   * Returns true if the API request should be retried, given the error that was
   * given the first time the request was attempted. This is used for rate limit
   * related errors as well as intermittent server errors.
   *
   * @param {error} err - The API error to check if it is appropriate to retry.
   * @return {boolean} True if the API request should be retried, false otherwise.
   */
  shouldRetryRequest(err?: ApiError) {
    if (err) {
      if ([408, 429, 500, 502, 503, 504].indexOf(err.code!) !== -1) {
        return true;
      }

      if (err.errors) {
        for (const e of err.errors) {
          const reason = e.reason;
          if (reason === 'rateLimitExceeded') {
            return true;
          }
          if (reason === 'userRateLimitExceeded') {
            return true;
          }
          if (reason && reason.includes('EAI_AGAIN')) {
            return true;
          }
        }
      }
    }

    return false;
  }

  /**
   * Get a function for making authenticated requests.
   *
   * @param {object} config - Configuration object.
   * @param {boolean=} config.autoRetry - Automatically retry requests if the
   *     response is related to rate limits or certain intermittent server
   * errors. We will exponentially backoff subsequent requests by default.
   * (default: true)
   * @param {object=} config.credentials - Credentials object.
   * @param {boolean=} config.customEndpoint - If true, just return the provided request options. Default: false.
   * @param {boolean=} config.useAuthWithCustomEndpoint - If true, will authenticate when using a custom endpoint. Default: false.
   * @param {string=} config.email - Account email address, required for PEM/P12 usage.
   * @param {number=} config.maxRetries - Maximum number of automatic retries attempted before returning the error. (default: 3)
   * @param {string=} config.keyFile - Path to a .json, .pem, or .p12 keyfile.
   * @param {array} config.scopes - Array of scopes required for the API.
   */
  makeAuthenticatedRequestFactory(
    config: MakeAuthenticatedRequestFactoryConfig
  ) {
    const googleAutoAuthConfig = extend({}, config);
    if (googleAutoAuthConfig.projectId === DEFAULT_PROJECT_ID_TOKEN) {
      delete googleAutoAuthConfig.projectId;
    }

    let authClient: GoogleAuth<AuthClient>;

    if (googleAutoAuthConfig.authClient instanceof GoogleAuth) {
      // Use an existing `GoogleAuth`
      authClient = googleAutoAuthConfig.authClient;
    } else {
      // Pass an `AuthClient` to `GoogleAuth`, if available
      const config = {
        ...googleAutoAuthConfig,
        authClient: googleAutoAuthConfig.authClient,
      };

      authClient = new GoogleAuth(config);
    }

    /**
     * The returned function that will make an authenticated request.
     *
     * @param {type} reqOpts - Request options in the format `request` expects.
     * @param {object|function} options - Configuration object or callback function.
     * @param {function=} options.onAuthenticated - If provided, a request will
     *     not be made. Instead, this function is passed the error &
     * authenticated request options.
     */
    function makeAuthenticatedRequest(
      reqOpts: DecorateRequestOptions
    ): Duplexify;
    function makeAuthenticatedRequest(
      reqOpts: DecorateRequestOptions,
      options?: MakeAuthenticatedRequestOptions
    ): void | Abortable;
    function makeAuthenticatedRequest(
      reqOpts: DecorateRequestOptions,
      callback?: BodyResponseCallback
    ): void | Abortable;
    function makeAuthenticatedRequest(
      reqOpts: DecorateRequestOptions,
      optionsOrCallback?: MakeAuthenticatedRequestOptions | BodyResponseCallback
    ): void | Abortable | Duplexify {
      let stream: Duplexify;
      let projectId: string;
      const reqConfig = extend({}, config);
      let activeRequest_: void | Abortable | null;

      if (!optionsOrCallback) {
        stream = duplexify();
        reqConfig.stream = stream;
      }

      const options =
        typeof optionsOrCallback === 'object' ? optionsOrCallback : undefined;
      const callback =
        typeof optionsOrCallback === 'function' ? optionsOrCallback : undefined;

      async function setProjectId() {
        projectId = await authClient.getProjectId();
      }

      const onAuthenticated = async (
        err: Error | null,
        authenticatedReqOpts?: DecorateRequestOptions
      ) => {
        const authLibraryError = err;
        const autoAuthFailed =
          err &&
          err.message.indexOf('Could not load the default credentials') > -1;

        if (autoAuthFailed) {
          // Even though authentication failed, the API might not actually
          // care.
          authenticatedReqOpts = reqOpts;
        }

        if (!err || autoAuthFailed) {
          try {
            // Try with existing `projectId` value
            authenticatedReqOpts = util.decorateRequest(
              authenticatedReqOpts!,
              projectId
            );

            err = null;
          } catch (e) {
            if (e instanceof MissingProjectIdError) {
              // A `projectId` was required, but we don't have one.
              try {
                // Attempt to get the `projectId`
                await setProjectId();

                authenticatedReqOpts = util.decorateRequest(
                  authenticatedReqOpts!,
                  projectId
                );

                err = null;
              } catch (e) {
                // Re-use the "Could not load the default credentials error" if
                // auto auth failed.
                err = err || (e as Error);
              }
            } else {
              // Some other error unrelated to missing `projectId`
              err = err || (e as Error);
            }
          }
        }

        if (err) {
          if (stream) {
            stream.destroy(err);
          } else {
            const fn =
              options && options.onAuthenticated
                ? options.onAuthenticated
                : callback;
            (fn as Function)(err);
          }
          return;
        }

        if (options && options.onAuthenticated) {
          options.onAuthenticated(null, authenticatedReqOpts);
        } else {
          activeRequest_ = util.makeRequest(
            authenticatedReqOpts!,
            reqConfig,
            (apiResponseError, ...params) => {
              if (
                apiResponseError &&
                (apiResponseError as ApiError).code === 401 &&
                authLibraryError
              ) {
                // Re-use the "Could not load the default credentials error" if
                // the API request failed due to missing credentials.
                apiResponseError = authLibraryError;
              }
              callback!(apiResponseError, ...params);
            }
          );
        }
      };

      const prepareRequest = async () => {
        try {
          const getProjectId = async () => {
            if (
              config.projectId &&
              config.projectId !== DEFAULT_PROJECT_ID_TOKEN
            ) {
              // The user provided a project ID. We don't need to check with the
              // auth client, it could be incorrect.
              return config.projectId;
            }

            if (config.projectIdRequired === false) {
              // A projectId is not required. Return the default.
              return DEFAULT_PROJECT_ID_TOKEN;
            }

            return setProjectId();
          };

          const authorizeRequest = async () => {
            if (
              reqConfig.customEndpoint &&
              !reqConfig.useAuthWithCustomEndpoint
            ) {
              // Using a custom API override. Do not use `google-auth-library` for
              // authentication. (ex: connecting to a local Datastore server)
              return reqOpts;
            } else {
              return authClient.authorizeRequest(reqOpts);
            }
          };

          const [_projectId, authorizedReqOpts] = await Promise.all([
            getProjectId(),
            authorizeRequest(),
          ]);

          if (_projectId) {
            projectId = _projectId;
          }

          return onAuthenticated(
            null,
            authorizedReqOpts as DecorateRequestOptions
          );
        } catch (e) {
          return onAuthenticated(e as Error);
        }
      };

      prepareRequest();

      if (stream!) {
        return stream!;
      }

      return {
        abort() {
          setImmediate(() => {
            if (activeRequest_) {
              activeRequest_.abort();
              activeRequest_ = null;
            }
          });
        },
      };
    }
    const mar = makeAuthenticatedRequest as MakeAuthenticatedRequest;
    mar.getCredentials = authClient.getCredentials.bind(authClient);
    mar.authClient = authClient;
    return mar;
  }

  /**
   * Make a request through the `retryRequest` module with built-in error
   * handling and exponential back off.
   *
   * @param {object} reqOpts - Request options in the format `request` expects.
   * @param {object=} config - Configuration object.
   * @param {boolean=} config.autoRetry - Automatically retry requests if the
   *     response is related to rate limits or certain intermittent server
   * errors. We will exponentially backoff subsequent requests by default.
   * (default: true)
   * @param {number=} config.maxRetries - Maximum number of automatic retries
   *     attempted before returning the error. (default: 3)
   * @param {object=} config.request - HTTP module for request calls.
   * @param {function} callback - The callback function.
   */
  makeRequest(
    reqOpts: DecorateRequestOptions,
    config: MakeRequestConfig,
    callback: BodyResponseCallback
  ): void | Abortable {
    let autoRetryValue = AUTO_RETRY_DEFAULT;
    if (
      config.autoRetry !== undefined &&
      config.retryOptions?.autoRetry !== undefined
    ) {
      throw new ApiError(
        'autoRetry is deprecated. Use retryOptions.autoRetry instead.'
      );
    } else if (config.autoRetry !== undefined) {
      autoRetryValue = config.autoRetry;
    } else if (config.retryOptions?.autoRetry !== undefined) {
      autoRetryValue = config.retryOptions.autoRetry;
    }

    let maxRetryValue = MAX_RETRY_DEFAULT;
    if (config.maxRetries && config.retryOptions?.maxRetries) {
      throw new ApiError(
        'maxRetries is deprecated. Use retryOptions.maxRetries instead.'
      );
    } else if (config.maxRetries) {
      maxRetryValue = config.maxRetries;
    } else if (config.retryOptions?.maxRetries) {
      maxRetryValue = config.retryOptions.maxRetries;
    }

    const options = {
      request: teenyRequest.defaults(requestDefaults),
      retries: autoRetryValue !== false ? maxRetryValue : 0,
      noResponseRetries: autoRetryValue !== false ? maxRetryValue : 0,
      shouldRetryFn(httpRespMessage: r.Response) {
        const err = util.parseHttpRespMessage(httpRespMessage).err;
        if (config.retryOptions?.retryableErrorFn) {
          return err && config.retryOptions?.retryableErrorFn(err);
        }
        return err && util.shouldRetryRequest(err);
      },
      maxRetryDelay: config.retryOptions?.maxRetryDelay,
      retryDelayMultiplier: config.retryOptions?.retryDelayMultiplier,
      totalTimeout: config.retryOptions?.totalTimeout,
    } as {} as retryRequest.Options;

    if (typeof reqOpts.maxRetries === 'number') {
      options.retries = reqOpts.maxRetries;
    }

    if (!config.stream) {
      return retryRequest(reqOpts, options, (err, response, body) => {
        util.handleResp(err, response as {} as r.Response, body, callback!);
      });
    }
    const dup = config.stream as AbortableDuplex;
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    let requestStream: any;
    const isGetRequest = (reqOpts.method || 'GET').toUpperCase() === 'GET';

    if (isGetRequest) {
      requestStream = retryRequest(reqOpts, options);
      dup.setReadable(requestStream);
    } else {
      // Streaming writable HTTP requests cannot be retried.
      requestStream = options.request!(reqOpts);
      dup.setWritable(requestStream);
    }

    // Replay the Request events back to the stream.
    requestStream
      .on('error', dup.destroy.bind(dup))
      .on('response', dup.emit.bind(dup, 'response'))
      .on('complete', dup.emit.bind(dup, 'complete'));

    dup.abort = requestStream.abort;
    return dup;
  }

  /**
   * Decorate the options about to be made in a request.
   *
   * @param {object} reqOpts - The options to be passed to `request`.
   * @param {string} projectId - The project ID.
   * @return {object} reqOpts - The decorated reqOpts.
   */
  decorateRequest(reqOpts: DecorateRequestOptions, projectId: string) {
    delete reqOpts.autoPaginate;
    delete reqOpts.autoPaginateVal;
    delete reqOpts.objectMode;

    if (reqOpts.qs !== null && typeof reqOpts.qs === 'object') {
      delete reqOpts.qs.autoPaginate;
      delete reqOpts.qs.autoPaginateVal;
      reqOpts.qs = replaceProjectIdToken(reqOpts.qs, projectId);
    }

    if (Array.isArray(reqOpts.multipart)) {
      reqOpts.multipart = (reqOpts.multipart as []).map(part => {
        return replaceProjectIdToken(part, projectId);
      });
    }

    if (reqOpts.json !== null && typeof reqOpts.json === 'object') {
      delete reqOpts.json.autoPaginate;
      delete reqOpts.json.autoPaginateVal;
      reqOpts.json = replaceProjectIdToken(reqOpts.json, projectId);
    }

    reqOpts.uri = replaceProjectIdToken(reqOpts.uri, projectId);

    return reqOpts;
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  isCustomType(unknown: any, module: string) {
    function getConstructorName(obj: Function) {
      return obj.constructor && obj.constructor.name.toLowerCase();
    }

    const moduleNameParts = module.split('/');

    const parentModuleName =
      moduleNameParts[0] && moduleNameParts[0].toLowerCase();
    const subModuleName =
      moduleNameParts[1] && moduleNameParts[1].toLowerCase();

    if (subModuleName && getConstructorName(unknown) !== subModuleName) {
      return false;
    }

    let walkingModule = unknown;
    // eslint-disable-next-line no-constant-condition
    while (true) {
      if (getConstructorName(walkingModule) === parentModuleName) {
        return true;
      }
      walkingModule = walkingModule.parent;
      if (!walkingModule) {
        return false;
      }
    }
  }

  /**
   * Create a properly-formatted User-Agent string from a package.json file.
   *
   * @param {object} packageJson - A module's package.json file.
   * @return {string} userAgent - The formatted User-Agent string.
   */
  getUserAgentFromPackageJson(packageJson: PackageJson) {
    const hyphenatedPackageName = packageJson.name
      .replace('@google-cloud', 'gcloud-node') // For legacy purposes.
      .replace('/', '-'); // For UA spec-compliance purposes.

    return hyphenatedPackageName + '/' + packageJson.version;
  }

  /**
   * Given two parameters, figure out if this is either:
   *  - Just a callback function
   *  - An options object, and then a callback function
   * @param optionsOrCallback An options object or callback.
   * @param cb A potentially undefined callback.
   */
  maybeOptionsOrCallback<T = {}, C = (err?: Error) => void>(
    optionsOrCallback?: T | C,
    cb?: C
  ): [T, C] {
    return typeof optionsOrCallback === 'function'
      ? [{} as T, optionsOrCallback as C]
      : [optionsOrCallback as T, cb as C];
  }
}

/**
 * Basic Passthrough Stream that records the number of bytes read
 * every time the cursor is moved.
 */
class ProgressStream extends Transform {
  bytesRead = 0;
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  _transform(chunk: any, encoding: string, callback: Function) {
    this.bytesRead += chunk.length;
    this.emit('progress', {bytesWritten: this.bytesRead, contentLength: '*'});
    this.push(chunk);
    callback();
  }
}

const util = new Util();
export {util};

results matching ""

    No results matching ""