Open tomekpaszek opened 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.
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.
@tomekpaszek Curious about what solution you have. This might apply to me too, thanks!
@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
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,
}),
}),
});
@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.
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