go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/src/common/hooks/prpc_query/use_prpc_service_client.ts (about)

     1  // Copyright 2024 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  import {
    16    useAuthState,
    17    useGetAccessToken,
    18  } from '@/common/components/auth_state_provider';
    19  import { PrpcClient } from '@/generic_libs/tools/prpc_client';
    20  import { Rpc } from '@/proto_utils/types';
    21  
    22  export interface PrpcServiceClientOptions<S, Params extends unknown[] = []> {
    23    /**
    24     * The host of the pRPC server.
    25     */
    26    readonly host: string;
    27    /**
    28     * If true, use HTTP instead of HTTPS. Defaults to `false`.
    29     */
    30    readonly insecure?: boolean;
    31    /**
    32     * A generic client implementation generated by ts-proto.
    33     * e.g. `UserClientImpl`. If the constructor requires additional params, they
    34     * can be provided via `usePrpcServiceClient(opts, ...params)`.
    35     */
    36    readonly ClientImpl: Constructor<S, [Rpc, ...Params]> & {
    37      readonly DEFAULT_SERVICE: string;
    38    };
    39  }
    40  
    41  /**
    42   * A pRPC method with optional additional params.
    43   */
    44  type Method<Req, Res, Params extends unknown[] = []> = (
    45    req: Req,
    46    ...params: Params
    47  ) => Res;
    48  
    49  /**
    50   * The request object from a pRPC method.
    51   */
    52  type MethodReq<T> = T extends Method<infer Req, infer _Res> ? Req : never;
    53  
    54  type PagedReq<Req> = Req extends { readonly pageToken: string } ? Req : never;
    55  type PagedRes<Res> = Awaited<Res> extends { readonly nextPageToken: string }
    56    ? Res
    57    : never;
    58  
    59  type MethodRes<T> = T extends Method<infer _Req, infer Res> ? Res : never;
    60  
    61  type MethodParams<T> = T extends Method<infer _Req, infer _Res, infer Params>
    62    ? Params
    63    : never;
    64  
    65  type MethodKeys<S, Req, Res, Params extends unknown[] = []> = keyof {
    66    [MK in keyof S as S[MK] extends Method<Req, Res, Params>
    67      ? MK
    68      : never]: unknown;
    69  };
    70  
    71  export type DecoratedMethod<MK, Req, Res, Params extends unknown[] = []> = {
    72    (req: Req, ...params: Params): Res;
    73    /**
    74     * Builds a ReactQuery option that queries the method. The query key is
    75     * consisted of
    76     * `[userIdentity, 'prpc', serviceHost, serviceName, methodName, request]`.
    77     */
    78    query(
    79      req: Req,
    80      ...params: Params
    81    ): {
    82      queryKey: [string, 'prpc', string, string, MK, Req];
    83      queryFn: () => Res;
    84    };
    85    /**
    86     * Builds a ReactQuery option that queries the paginated method. The next page
    87     * param is automatically extracted from the previous response. The query key
    88     * is consisted of
    89     * `[userIdentity, 'prpc-paged', serviceHost, serviceName, methodName, request]`.
    90     */
    91    queryPaged(
    92      req: PagedRes<Res> extends Res ? PagedReq<Req> : never,
    93      ...params: Params
    94    ): {
    95      queryKey: [string, 'prpc-paged', string, string, MK, Req];
    96      queryFn: (ctx: { pageParam?: string }) => Res;
    97      getNextPageParam: (lastRes: Awaited<Res>) => string | null;
    98    };
    99  };
   100  
   101  export type DecoratedClient<S> = {
   102    // The request type has to be `any` because the argument type must be contra-
   103    // variant when sub-typing a function.
   104    // eslint-disable-next-line @typescript-eslint/no-explicit-any
   105    [MK in keyof S]: S[MK] extends Method<any, any>
   106      ? DecoratedMethod<
   107          MK,
   108          MethodReq<S[MK]>,
   109          MethodRes<S[MK]>,
   110          MethodParams<S[MK]>
   111        >
   112      : S[MK];
   113  };
   114  
   115  export interface CallOpt<MK, Request> {
   116    readonly method: MK;
   117    readonly request: Request;
   118  }
   119  
   120  /**
   121   * Construct a decorated pRPC client with the users credentials. Add helper
   122   * functions to each pRPC method to help generating ReactQuery options.
   123   *
   124   * Example:
   125   * ```typescript
   126   * // Constructs a client.
   127   * const client = usePrpcClient({
   128   *   // The host of the pRPC server.
   129   *   host: 'cr-buildbucket-dev.appspot.com',
   130   *   // The client implementation generated by ts-proto.
   131   *   ClientImpl: BuildsClientImpl,
   132   * })
   133   *
   134   * // Use the `.query` helper function to build a ReactQuery.
   135   * const {data, isLoading, ...} = useQuery({
   136   *   // `.query` is available on all pRPC methods. It takes the same parameters
   137   *   // as the method itself, and returns a ReactQuery options object with
   138   *   // `queryKey` and `queryFn` populated.
   139   *   ...client.GetBuild.query({
   140   *     // A type checked request object. The type of the request object is
   141   *     // inferred from the supplied client implementation and method name.
   142   *     id: "1234",
   143   *     ...
   144   *   }),
   145   *   // Add or override query options if needed.
   146   *   select: (res) => {...},
   147   *   ...
   148   * })
   149   *
   150   * // Use the `.queryPaged` helper function to build an infinite ReactQuery.
   151   * const {data, isLoading, ...} = useInfiniteQuery({
   152   *   // `.queryPaged` similar to `.query` except that
   153   *   // 1. it is only available on pRPC methods that have 'pageToken' in the
   154   *   // request object and 'nextPageToken' in the response object, and
   155   *   // 2. in addition to `queryKey` and `queryFn`, it also populates
   156   *   // `getNextPageParam`, making it suitable for `useInfiniteQuery`.
   157   *   ...client.SearchBuilds.queryPaged({
   158   *     // A type checked request object. The type of the request object is
   159   *     // inferred from the supplied client implementation and method name.
   160   *     id: "1234",
   161   *     ...
   162   *   }),
   163   *   // Add or override query options if needed.
   164   *   select: (res) => {...},
   165   *   ...
   166   * })
   167   * ```
   168   */
   169  export function usePrpcServiceClient<
   170    S extends object,
   171    Params extends unknown[],
   172  >(opts: PrpcServiceClientOptions<S, Params>, ...params: Params) {
   173    const { host, insecure, ClientImpl } = opts;
   174  
   175    const { identity } = useAuthState();
   176    const getAuthToken = useGetAccessToken();
   177    const client = new ClientImpl(
   178      new PrpcClient({ host, insecure, getAuthToken }),
   179      ...params,
   180    );
   181  
   182    return new Proxy(client, {
   183      get(target, p, receiver) {
   184        const mk = p as MethodKeys<S, unknown, unknown>;
   185        const value = Reflect.get(target, mk, receiver);
   186        if (typeof value !== 'function') {
   187          return value;
   188        }
   189        const fn = (...args: unknown[]) => value.apply(target, args);
   190        fn.query = (req: object, ...params: unknown[]) => ({
   191          queryKey: [identity, 'prpc', host, ClientImpl.DEFAULT_SERVICE, mk, req],
   192          queryFn: () => fn(req, ...params),
   193        });
   194        fn.queryPaged = (req: object, ...params: unknown[]) => ({
   195          queryKey: [
   196            identity,
   197            'prpc-paged',
   198            host,
   199            ClientImpl.DEFAULT_SERVICE,
   200            mk,
   201            req,
   202          ],
   203          queryFn: ({ pageParam = '' }) =>
   204            fn(pageParam ? { ...req, pageToken: pageParam } : req, ...params),
   205          // Return `null` when the next page token is an empty string so it get
   206          // treated as no next page.
   207          getNextPageParam: ({ nextPageToken = '' }) => nextPageToken || null,
   208        });
   209        return fn;
   210      },
   211    }) as DecoratedClient<S>;
   212  }