apollo-elements/apollo-elements

View on GitHub
packages/mixins/apollo-query-mixin.ts

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
import type * as C from '@apollo/client/core';

import type {
  ComponentDocument,
  Constructor,
  Data,
  FetchMoreParams,
  NextFetchPolicyFunction,
  Variables,
  VariablesOf,
} from '@apollo-elements/core/types';

import type { ApolloQueryElement } from '@apollo-elements/core/types';

import { NetworkStatus } from '@apollo/client/core';

import { ApolloElementMixin } from './apollo-element-mixin';
import { dedupeMixin } from '@open-wc/dedupe-mixin';
import { controlled } from '@apollo-elements/core/decorators';

import {
  ApolloQueryController,
  ApolloQueryControllerOptions,
} from '@apollo-elements/core/apollo-query-controller';

type MixinInstance<B extends Constructor> = B & {
  new <D, V = VariablesOf<D>>(...a: any[]): InstanceType<B> & ApolloQueryElement<D, V>;
  documentType: 'query',
}

/**
 * `ApolloQueryMixin`: class mixin for apollo-query elements.
 */
function ApolloQueryMixinImpl<B extends Constructor>(superclass: B): MixinInstance<B> {
  class ApolloQueryElement<D = unknown, V = VariablesOf<D>>
    extends ApolloElementMixin(superclass)<D, V> {
    static override documentType: 'query' = 'query';

    static override get observedAttributes(): string[] {
      return [
        ...super.observedAttributes as string[],
        'next-fetch-policy',
        'no-auto-subscribe',
      ];
    }

    controller = new ApolloQueryController<D, V>(this, null, {
      shouldSubscribe: x => this.readyToReceiveDocument && this.shouldSubscribe(x),
      onData: data => this.onData?.(data), /* c8 ignore next */ // covered
      onError: error => this.onError?.(error), /* c8 ignore next */ // covered
    });

    /** @summary The latest query data. */
    declare data: Data<D> | null;

    /**
     * An object map from variable name to variable value, where the variables are used within the GraphQL query.
     *
     * Setting variables will initiate the query, unless [`noAutoSubscribe`](#noautosubscribe) is also set.
     *
     * @summary Query variables.
     */
    declare variables: Variables<D, V> | null;

    get options(): ApolloQueryControllerOptions<D, V> {
      return this.controller.options;
    }

    set options(options: ApolloQueryControllerOptions<D, V>) {
      const { onData, onError } = this.controller.options;
      this.controller.options = {
        ...options,
        onData,
        onError,
      };
    }

    /**
     * If data was read from the cache with missing fields,
     * partial will be true. Otherwise, partial will be falsy.
     *
     * @summary True when the query returned partial data.
     */
    @controlled({ readonly: true }) readonly partial = false;

    /**
     * `networkStatus` is useful if you want to display a different loading indicator (or no indicator at all)
     * depending on your network status as it provides a more detailed view into the state of a network request
     * on your component than `loading` does. `networkStatus` is an enum with different number values between 1 and 8.
     * These number values each represent a different network state.
     *
     * 1. `loading`: The query has never been run before and the request is now pending. A query will still have this network status even if a result was returned from the cache, but a query was dispatched anyway.
     * 2. `setVariables`: If a query’s variables change and a network request was fired then the network status will be setVariables until the result of that query comes back. React users will see this when options.variables changes on their queries.
     * 3. `fetchMore`: Indicates that fetchMore was called on this query and that the network request created is currently in flight.
     * 4. `refetch`: It means that refetch was called on a query and the refetch request is currently in flight.
     * 5. Unused.
     * 6. `poll`: Indicates that a polling query is currently in flight. So for example if you are polling a query every 10 seconds then the network status will switch to poll every 10 seconds whenever a poll request has been sent but not resolved.
     * 7. `ready`: No request is in flight for this query, and no errors happened. Everything is OK.
     * 8. `error`: No request is in flight for this query, but one or more errors were detected.
     *
     * If the network status is less then 7 then it is equivalent to `loading` being true. In fact you could
     * replace all of your `loading` checks with `networkStatus < 7` and you would not see a difference.
     * It is recommended that you use `loading`, however.
     */
    @controlled() networkStatus: NetworkStatus = NetworkStatus.ready;

    /** @summary A GraphQL document containing a single query. */
    @controlled() query: ComponentDocument<D, V> | null = null;

    /**
     * Determines where the client may return a result from. The options are:
     *
     * - `cache-first` (default): return result from cache, fetch from network if cached result is not available.
     * - `cache-and-network`: return result from cache first (if it exists), then return network result once it's available.
     * - `cache-only`: return result from cache if available, fail otherwise.
     * - `no-cache`: return result from network, fail if network call doesn't succeed, don't save to cache
     * - `network-only`: return result from network, fail if network call doesn't succeed, save to cache
     * - `standby`: only for queries that aren't actively watched, but should be available for refetch and updateQueries.
     *
     * @summary The [fetchPolicy](https://www.apollographql.com/docs/react/api/core/ApolloClient/#FetchPolicy) for the query.
     * @attr fetch-policy
     */
    @controlled({ path: 'options' }) fetchPolicy?: C.WatchQueryFetchPolicy;

    /**
     * If true, perform a query refetch if the query result is marked as being partial,
     * and the returned data is reset to an empty Object by the Apollo Client QueryManager
     * (due to a cache miss).
     *
     * The default value is false for backwards-compatibility's sake,
     * but should be changed to true for most use-cases.
     *
     * @summary Set to retry any partial query results.
     */
    @controlled({ path: 'options' }) partialRefetch?: boolean;

    /**
     * Opt into receiving partial results from the cache for queries
     * that are not fully satisfied by the cache.
     */
    @controlled({ path: 'options' }) returnPartialData?: boolean;

    /**
     * When someone chooses cache-and-network or network-only as their
     * initial FetchPolicy, they often do not want future cache updates to
     * trigger unconditional network requests, which is what repeatedly
     * applying the cache-and-network or network-only policies would seem
     * to imply. Instead, when the cache reports an update after the
     * initial network request, it may be desirable for subsequent network
     * requests to be triggered only if the cache result is incomplete.
     * The nextFetchPolicy option provides a way to update
     * options.fetchPolicy after the intial network request, without
     * having to set options.
     *
     * @summary Set to prevent subsequent network requests when the fetch policy is `cache-and-network` or `network-only`.
     * @attr next-fetch-policy
     */
    @controlled({
      path: 'options',
      onSet(this: ApolloQueryElement, value: ApolloQueryElement['nextFetchPolicy']) {
        if (value && typeof value !== 'function')
          this.setAttribute('next-fetch-policy', value); /* c8 ignore next */ // covered
        else
          this.removeAttribute('next-fetch-policy');
      },
    })
      nextFetchPolicy?: C.WatchQueryFetchPolicy | NextFetchPolicyFunction<D, V>;

    /**
     * When true, the component will not automatically subscribe to new data.
     * Call the `subscribe()` method to do so.
     * @attr no-auto-subscribe
     */
    @controlled({
      path: 'options',
      onSet(this: ApolloQueryElement, value: ApolloQueryElement['noAutoSubscribe']) {
        this.toggleAttribute('no-auto-subscribe', !!value);
      },
    })
      noAutoSubscribe = this.hasAttribute('no-auto-subscribe');

    /**
     * @summary Whether or not updates to the network status should trigger next on the observer of this query.
     */
    @controlled({ path: 'options' }) notifyOnNetworkStatusChange?: boolean;

    /** @summary The time interval (in milliseconds) on which this query should be refetched from the server. */
    @controlled({ path: 'options' }) pollInterval?: number;

    /**
     * Optional callback for when a query is completed.
     * @param data the query data.
     */
    onData?(data: Data<D>): void

    /**
     * Optional callback for when an error occurs.
     * @param error the error.
     */
    onError?(error: Error): void

    /** @summary Flags an element that's ready and able to auto subscribe */
    @controlled({ readonly: true }) readonly canAutoSubscribe = true;

    override attributeChangedCallback(name: string, oldVal: string, newVal: string): void {
      super.attributeChangedCallback?.(name, oldVal, newVal);
      // @ts-expect-error: ts is not tracking the static side
      if (super.constructor?.observedAttributes?.includes?.(name))
        return; /* c8 ignore next */ // covered

      switch (name) { /* c8 ignore next */ // covered
        case 'next-fetch-policy':
          // to allow for case where this.nextFetchPolicy is undefined
          // eslint-disable-next-line eqeqeq
          if (this.nextFetchPolicy != newVal)
            this.nextFetchPolicy = newVal as C.WatchQueryFetchPolicy; /* c8 ignore next */ // covered
          break;
        case 'no-auto-subscribe':
          this.noAutoSubscribe = newVal != null;
      }
    }

    /**
     * Exposes the [`ObservableQuery#refetch`](https://www.apollographql.com/docs/react/api/apollo-client.html#ObservableQuery.refetch) method.
     *
     * @param variables The new set of variables. If there are missing variables, the previous values of those variables will be used..
     */
    async refetch(variables: Variables<D, V>): Promise<C.ApolloQueryResult<Data<D>>> {
      return this.controller.refetch(variables);
    }

    /**
     * Determines whether the element should attempt to automatically subscribe i.e. begin querying
     *
     * Override to prevent subscribing unless your conditions are met.
     */
    shouldSubscribe(
      options?: Partial<C.SubscriptionOptions<Variables<D, V>, Data<D>>>
    ): boolean {
      return (void options, true);
    }

    /**
     * Resets the observableQuery and subscribes.
     * @param params options for controlling how the subscription subscribes
     */
    subscribe(
      params?: Partial<C.SubscriptionOptions<Variables<D, V>, Data<D>>>
    ): C.ObservableSubscription {
      return this.controller.subscribe(params);
    }

    /**
     * Lets you pass a GraphQL subscription and updateQuery function
     * to subscribe to more updates for your query.
     *
     * The `updateQuery` parameter is a function that takes the previous query data,
     * then a `{ subscriptionData: TSubscriptionResult }` object,
     * and returns an object with updated query data based on the new results.
     */
    subscribeToMore<TSubscriptionVariables, TSubscriptionData>(
      options: C.SubscribeToMoreOptions<Data<D>, TSubscriptionVariables, TSubscriptionData>
    ): void | (() => void) {
      return this.controller.subscribeToMore(options);
    }

    /**
     * Executes a Query once and updates the component with the result
     */
    async executeQuery(
      params?: Partial<C.QueryOptions<Variables<D, V>, Data<D>>> | undefined
    ): Promise<C.ApolloQueryResult<Data<D>>> {
      return this.controller.executeQuery(params);
    }

    /**
     * Exposes the `ObservableQuery#fetchMore` method.
     * https://www.apollographql.com/docs/react/api/core/ObservableQuery/#ObservableQuery.fetchMore
     *
     * The optional `updateQuery` parameter is a function that takes the previous query data,
     * then a `{ subscriptionData: TSubscriptionResult }` object,
     * and returns an object with updated query data based on the new results.
     *
     * The optional `variables` parameter is an optional new variables object.
     */
    async fetchMore(
      params?: Partial<FetchMoreParams<D, V>> | undefined
    ): Promise<C.ApolloQueryResult<Data<D>>> {
      return this.controller.fetchMore(params);
    }
  }

  return ApolloQueryElement as unknown as MixinInstance<B>;
}


export const ApolloQueryMixin =
  dedupeMixin(ApolloQueryMixinImpl);