lukasmoellerch / rtk-query-grpc

MIT License
4 stars 2 forks source link

Incompatibility with the latest redux toolkit version #1

Open tomekpaszek opened 2 years ago

tomekpaszek commented 2 years ago

Hi!

I've stumbled upon this package and tried to make it work with latest redux toolkit. After updating references to use @reduxjs/toolkit some more errors appear that I'm having difficulties solving. The issues are related to type being incompatible but unfortunately my TS skills are not good enough to figure it out.

Is there any plan for updating this package?

Best Regards, Tomek

lukasmoellerch commented 2 years ago

This package is an overly complicated workaround for a Limitation which rtk-query no longer has. Previously having a custom queryFn for each query wasn't support so I had to create a global query function which calls the grpc methods. With the addition of queryFn it should be much easier to integrate grpc web APIs.

The package would require a major overhaul and I'd have to reconsider the design of the API, for now this was mostly an experiment, showing that it was possible to circumvent the API limitation. I currently don't have time to work on this project, but I'd happily accept and review pull requests. When I have to use a grpc web api again I might come back to this project, but for the time being I probably won't be working on it.

tomekpaszek commented 2 years ago

Thank you for the answer, Lukas. I started digging into rtk-query and came up with a solution that satisfies me. If I find time to make the solution general enough, I will happily make PR.

santhosh-c1 commented 1 year ago

@tomekpaszek Curious about what solution you have. This might apply to me too, thanks!

tomekpaszek commented 1 year ago

@tomekpaszek Curious about what solution you have. This might apply to me too, thanks!

Hi! I do have a solution that I'm quite happy with. When I find a moment I will share the code

tomekpaszek commented 1 year ago

Hi! I don't have time to make a proper PR, but I will paste code snippets here. I hope it will still be helpful. It requires version >=1.9.0 of redux toolkit and grpc-web generated client class.

Let me know if you have any questions!

import { isPlainObject } from '@reduxjs/toolkit';
import { MutationDefinition } from '@reduxjs/toolkit/dist/query/react';
import * as jspb from 'google-protobuf';
import { Empty } from 'google-protobuf/google/protobuf/empty_pb';
import { RpcError as GrpcError } from 'grpc-web';
import { getImpersonatedUser } from 'src/hooks/useRBAC';
import {
  AsObject,
  ClientClass,
  CreateGrpcMutationProps,
  CreateGrpcQueryProps,
  GrpcBaseQuery,
  GrpcQueryDefinition,
  GrpcQueryExtraOptions,
} from 'src/store/grpcWrappers/types';
import { grpcOptionsForEndpoint } from 'src/utils/juniperClient';

export const grpcSerializeQueryArgs = ({ queryArgs, endpointName }) => {
  if (queryArgs === undefined) return '';
  return `${endpointName}(${JSON.stringify(queryArgs, (key, value) =>
    isPlainObject(value)
      ? Object.keys(value)
          .sort()
          .reduce<any>((acc, key) => {
            acc[key] = (value as any)[key];
            return acc;
          }, {})
      : value,
  )})`;
};

const getMetadata = () => {
  return {};
};

export const GetGrpcBaseQuery =
  (clientClass: ClientClass, endpoint: string): GrpcBaseQuery =>
  async (arg, api, extraOptions) => {
    try {
      const client = new clientClass(
        endpoint,
        null,
        grpcOptionsForEndpoint(endpoint),
      );

      let transformedArgs = arg ?? new Empty();
      if (extraOptions?.prepareArgs) {
        transformedArgs = extraOptions.prepareArgs(arg);
      }

      const data = await extraOptions.endpointMethod.call(
        client,
        transformedArgs,
        getMetadata(),
      );
      return { data };
    } catch (err) {
      //Http response at 400 or 500 level
      return {
        error: err as GrpcError,
      };
    }
  };

export const CreateGrpcQuery = <
  // grpc api request type
  RequestType extends jspb.Message,
  // grpc api request type
  ResponseType extends jspb.Message,
  // the type that should be persisted
  ResultType = AsObject<ResponseType>,
>(
  props: CreateGrpcQueryProps<RequestType, ResponseType, ResultType>,
): GrpcQueryDefinition<RequestType, any> => {
  const { builder, transformResult } = props;

  return builder.query<ResultType, AsObject<RequestType>>({
    extraOptions: {
      ...(props as GrpcQueryExtraOptions),
    },
    query: (req) => req,
    transformResponse: (response: ResponseType, meta, arg): ResultType =>
      transformResult
        ? transformResult(response.toObject(), meta, arg)
        : response.toObject(),
    ...props,
  });
};

export const CreateGrpcMutation = <
  RequestType extends jspb.Message,
  ResponseType extends jspb.Message,
>(
  props: CreateGrpcMutationProps<RequestType, ResponseType>,
): MutationDefinition<RequestType, GrpcBaseQuery, string, ResponseType> => {
  const { builder } = props;
  return builder.mutation<ResponseType, RequestType>({
    extraOptions: {
      ...(props as GrpcQueryExtraOptions),
    },
    query: (req) => req,
    ...props,
  });
};
import {
  BaseQueryError,
  BaseQueryMeta,
} from '@reduxjs/toolkit/dist/query/baseQueryTypes';
import { MutationResultSelectorResult } from '@reduxjs/toolkit/dist/query/core/buildSelectors';
import {
  EndpointBuilder,
  MutationLifecycleApi,
  QueryLifecycleApi,
} from '@reduxjs/toolkit/dist/query/endpointDefinitions';
import {
  BaseQueryFn,
  MutationDefinition,
  QueryDefinition,
} from '@reduxjs/toolkit/dist/query/react';
import {
  MutationTrigger,
  UseLazyQuery,
  UseMutation,
  UseMutationStateResult,
  UseQuery,
} from '@reduxjs/toolkit/dist/query/react/buildHooks';
import { ResultDescription } from '@reduxjs/toolkit/src/query/endpointDefinitions';
import * as jspb from 'google-protobuf';
import { RpcError as GrpcError } from 'grpc-web';

//
// General
//

export interface GrpcMessage {
  toObject(): any;
  serializeBinary: () => Uint8Array;
}

export type AsObject<A> = A extends GrpcMessage ? ReturnType<A['toObject']> : A;

export type ClientClass = new (str: string, x: any, opts: object) => Object;

export type GrpcQueryExtraOptions = {
  endpointMethod?: (req: jspb.Message, metadata: {}) => Promise<jspb.Message>;
  prepareArgs?: (args: jspb.Message) => jspb.Message;
};

export type GrpcBaseQuery = BaseQueryFn<
  jspb.Message,
  unknown,
  GrpcError,
  GrpcQueryExtraOptions
>;

//
// Queries
//

export type GrpcQueryDefinition<QueryArgs, ResultType> = QueryDefinition<
  QueryArgs,
  GrpcBaseQuery,
  string,
  ResultType,
  string
>;

export type CreateGrpcQueryProps<QueryArg, ResponseType, ResultType> = {
  builder: EndpointBuilder<GrpcBaseQuery, string, string>;
  endpointMethod: (
    req: jspb.Message,
    metadata: jspb.Metadata,
  ) => Promise<ResponseType>;
  prepareArgs?: (args: QueryArg) => QueryArg;
  keepUnusedDataFor?: number;
  transformResult?: (
    respObj: AsObject<ResponseType>,
    meta: BaseQueryMeta<GrpcBaseQuery>,
    arg: any,
  ) => ResultType;
  providesTags?: ResultDescription<
    string,
    ResultType,
    AsObject<QueryArg>,
    GrpcError,
    BaseQueryMeta<any>
  >;
  onQueryStarted?: OnQueryStartedType<QueryArg, ResultType>;
};

export type OnQueryStartedType<QueryArg, ResultType> = (
  arg: QueryArg,
  api: QueryLifecycleApi<QueryArg, GrpcBaseQuery, ResultType, string>,
) => Promise<void> | void;

//
// Mutations
//

export type GrpcMutationDefinition<QueryArgs, ResultType> = MutationDefinition<
  QueryArgs,
  GrpcBaseQuery,
  string,
  ResultType
>;

export type CreateGrpcMutationProps<QueryArg, ResponseType> = {
  builder: EndpointBuilder<GrpcBaseQuery, string, string>;
  endpointMethod: (
    req: jspb.Message,
    metadata: jspb.Metadata,
  ) => Promise<ResponseType>;
  prepareArgs?: (args: QueryArg) => QueryArg;
  invalidatesTags?: ResultDescription<
    string,
    ResponseType,
    QueryArg,
    BaseQueryError<GrpcBaseQuery>,
    BaseQueryMeta<any>
  >;
  onQueryStarted?: OnMutationStartedType<QueryArg, ResponseType>;
};

export type OnMutationStartedType<QueryArg, ResultType> = (
  arg: QueryArg,
  api: MutationLifecycleApi<QueryArg, GrpcBaseQuery, ResultType, string>,
) => Promise<void> | void;

Usage examples:

export const api = createApi({
  reducerPath: 'myApi',
  baseQuery: GetGrpcBaseQuery(MyClient, grpcEndpoint),
  serializeQueryArgs: grpcSerializeQueryArgs,
  endpoints: (builder) => ({
    getData: CreateGrpcQuery<
      DataRequest,
      DataResult,
      DataResult.AsObject
    >({
      builder,
      endpointMethod: MyClient.prototype.getData,
    }),
  }),
});
santhosh-c1 commented 1 year ago

@tomekpaszek First off, thanks so much for your reply and taking the time to post this code snippet. Highly appreciate it! This helps a lot - I'll report back here on how it works for me.