Skip to content

Add datatag to ConnectQueryKey so queryClient can infer type info #532

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
May 20, 2025
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
node_modules
/packages/*/dist
/packages/*/coverage
tsconfig.vitest-temp.json
96 changes: 93 additions & 3 deletions packages/connect-query-core/src/connect-query-key.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,20 @@

import { create } from "@bufbuild/protobuf";
import type { Transport } from "@connectrpc/connect";
import { ElizaService, SayRequestSchema } from "test-utils/gen/eliza_pb.js";
import {
ElizaService,
SayRequestSchema,
SayResponseSchema,
type SayResponse,
} from "test-utils/gen/eliza_pb.js";
import { ListRequestSchema, ListService } from "test-utils/gen/list_pb.js";
import { describe, expect, it } from "vitest";
import { describe, expect, expectTypeOf, it } from "vitest";

import { createConnectQueryKey } from "./connect-query-key.js";
import { skipToken } from "./index.js";
import { createMessageKey } from "./message-key.js";
import { createTransportKey } from "./transport-key.js";
import { type InfiniteData, QueryClient } from "@tanstack/query-core";

describe("createConnectQueryKey", () => {
const fakeTransport: Transport = {
Expand Down Expand Up @@ -125,12 +131,96 @@ describe("createConnectQueryKey", () => {

it("cannot except invalid input", () => {
createConnectQueryKey({
// @ts-expect-error(2322) cannot create a key with invalid input
schema: ElizaService.method.say,
input: {
// @ts-expect-error(2322) cannot create a key with invalid input
sentence: 1,
},
cardinality: undefined,
});
});

it("contains type hints to indicate the output type", () => {
const sampleQueryClient = new QueryClient();
const key = createConnectQueryKey({
schema: ElizaService.method.say,
input: create(SayRequestSchema, { sentence: "hi" }),
cardinality: "finite",
});
const data = sampleQueryClient.getQueryData(key);

expectTypeOf(data).toEqualTypeOf<SayResponse | undefined>();
});

it("supports typesafe data updaters", () => {
const sampleQueryClient = new QueryClient();
const key = createConnectQueryKey({
schema: ElizaService.method.say,
input: create(SayRequestSchema, { sentence: "hi" }),
cardinality: "finite",
});
// @ts-expect-error(2345) this is a test to check if the type is correct
sampleQueryClient.setQueryData(key, { sentence: 1 });
// @ts-expect-error(2345) $typename is required
sampleQueryClient.setQueryData(key, {
sentence: "a proper value but missing $typename",
});
sampleQueryClient.setQueryData(
key,
create(SayResponseSchema, { sentence: "a proper value" }),
);

sampleQueryClient.setQueryData(key, (prev) => {
expectTypeOf(prev).toEqualTypeOf<SayResponse | undefined>();
return create(SayResponseSchema, {
sentence: "a proper value",
});
});
});

describe("infinite queries", () => {
it("contains type hints to indicate the output type", () => {
const sampleQueryClient = new QueryClient();
const key = createConnectQueryKey({
schema: ElizaService.method.say,
input: create(SayRequestSchema, { sentence: "hi" }),
cardinality: "infinite",
});
const data = sampleQueryClient.getQueryData(key);

expectTypeOf(data).toEqualTypeOf<InfiniteData<SayResponse> | undefined>();
});

it("supports typesafe data updaters", () => {
const sampleQueryClient = new QueryClient();
const key = createConnectQueryKey({
schema: ElizaService.method.say,
input: create(SayRequestSchema, { sentence: "hi" }),
cardinality: "infinite",
});
sampleQueryClient.setQueryData(key, {
pages: [
// @ts-expect-error(2345) make sure the shape is as expected
{ sentence: 1 },
],
});
sampleQueryClient.setQueryData(key, {
// @ts-expect-error(2345) $typename is required
pages: [{ sentence: "a proper value but missing $typename" }],
});
sampleQueryClient.setQueryData(key, {
pageParams: [0],
pages: [create(SayResponseSchema, { sentence: "a proper value" })],
});
sampleQueryClient.setQueryData(key, (prev) => {
expectTypeOf(prev).toEqualTypeOf<
InfiniteData<SayResponse> | undefined
>();
return {
pageParams: [0],
pages: [create(SayResponseSchema, { sentence: "a proper value" })],
};
});
});
});
});
129 changes: 95 additions & 34 deletions packages/connect-query-core/src/connect-query-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,60 @@ import type {
DescMethodUnary,
DescService,
MessageInitShape,
MessageShape,
} from "@bufbuild/protobuf";
import type { Transport } from "@connectrpc/connect";
import type { SkipToken } from "@tanstack/query-core";
import type { ConnectError, Transport } from "@connectrpc/connect";
import type { DataTag, InfiniteData, SkipToken } from "@tanstack/query-core";

import { createMessageKey } from "./message-key.js";
import { createTransportKey } from "./transport-key.js";

type SharedConnectQueryOptions = {
/**
* A key for a Transport reference, created with createTransportKey().
*/
transport?: string;
/**
* The name of the service, e.g. connectrpc.eliza.v1.ElizaService
*/
serviceName: string;
/**
* The name of the method, e.g. Say.
*/
methodName?: string;
/**
* A key for the request message, created with createMessageKey(),
* or "skipped".
*/
input?: Record<string, unknown> | "skipped";
};

type InfiniteConnectQueryKey<OutputMessage extends DescMessage = DescMessage> =
DataTag<
[
"connect-query",
SharedConnectQueryOptions & {
/** This data represents a infinite, paged result */
cardinality: "infinite";
},
],
InfiniteData<MessageShape<OutputMessage>>,
ConnectError
>;

type FiniteConnectQueryKey<OutputMessage extends DescMessage = DescMessage> =
DataTag<
[
"connect-query",
SharedConnectQueryOptions & {
/** This data represents a finite result */
cardinality: "finite";
},
],
MessageShape<OutputMessage>,
ConnectError
>;

/**
* TanStack Query manages query caching for you based on query keys. `QueryKey`s in TanStack Query are arrays with arbitrary JSON-serializable data - typically handwritten for each endpoint.
*
Expand All @@ -44,35 +91,15 @@ import { createTransportKey } from "./transport-key.js";
* }
* ]
*/
export type ConnectQueryKey = [
/**
* To distinguish Connect query keys from other query keys, they always start with the string "connect-query".
*/
"connect-query",
{
/**
* A key for a Transport reference, created with createTransportKey().
*/
transport?: string;
/**
* The name of the service, e.g. connectrpc.eliza.v1.ElizaService
*/
serviceName: string;
/**
* The name of the method, e.g. Say.
*/
methodName?: string;
/**
* A key for the request message, created with createMessageKey(),
* or "skipped".
*/
input?: Record<string, unknown> | "skipped";
/**
* Whether this is an infinite query, or a regular one.
*/
cardinality?: "infinite" | "finite" | undefined;
},
];
export type ConnectQueryKey<OutputMessage extends DescMessage = DescMessage> =
| InfiniteConnectQueryKey<OutputMessage>
| FiniteConnectQueryKey<OutputMessage>
| [
"connect-query",
SharedConnectQueryOptions & {
cardinality: undefined;
},
];

type KeyParamsForMethod<Desc extends DescMethod> = {
/**
Expand Down Expand Up @@ -152,14 +179,48 @@ type KeyParamsForService<Desc extends DescService> = {
*
* @see ConnectQueryKey for information on the components of Connect-Query's keys.
*/
export function createConnectQueryKey<
I extends DescMessage,
O extends DescMessage,
>(
params: KeyParamsForMethod<DescMethodUnary<I, O>> & {
cardinality: "finite";
},
): FiniteConnectQueryKey<O>;
export function createConnectQueryKey<
I extends DescMessage,
O extends DescMessage,
>(
params: KeyParamsForMethod<DescMethodUnary<I, O>> & {
cardinality: "infinite";
},
): InfiniteConnectQueryKey<O>;
export function createConnectQueryKey<
I extends DescMessage,
O extends DescMessage,
>(
params: KeyParamsForMethod<DescMethodUnary<I, O>> & {
cardinality: undefined;
},
): ConnectQueryKey<O>;
export function createConnectQueryKey<
O extends DescMessage,
Desc extends DescService,
>(params: KeyParamsForService<Desc>): ConnectQueryKey<O>;
export function createConnectQueryKey<
I extends DescMessage,
O extends DescMessage,
Desc extends DescService,
>(
params: KeyParamsForMethod<DescMethodUnary<I, O>> | KeyParamsForService<Desc>,
): ConnectQueryKey {
const props: ConnectQueryKey[1] =
): ConnectQueryKey<O> {
const props: {
serviceName: string;
methodName?: string;
transport?: string;
cardinality?: "finite" | "infinite";
input?: "skipped" | Record<string, unknown>;
} =
params.schema.kind == "rpc"
? {
serviceName: params.schema.parent.typeName,
Expand All @@ -185,5 +246,5 @@ export function createConnectQueryKey<
);
}
}
return ["connect-query", props];
return ["connect-query", props] as ConnectQueryKey<O>;
}
16 changes: 8 additions & 8 deletions packages/connect-query-core/src/create-infinite-query-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ function createUnaryInfiniteQueryFn<
},
): QueryFunction<
MessageShape<O>,
ConnectQueryKey,
ConnectQueryKey<O>,
MessageInitShape<I>[ParamKey]
> {
return async (context) => {
Expand Down Expand Up @@ -104,10 +104,10 @@ export function createInfiniteQueryOptions<
O,
ParamKey
>["getNextPageParam"];
queryKey: ConnectQueryKey;
queryKey: ConnectQueryKey<O>;
queryFn: QueryFunction<
MessageShape<O>,
ConnectQueryKey,
ConnectQueryKey<O>,
MessageInitShape<I>[ParamKey]
>;
structuralSharing: (oldData: unknown, newData: unknown) => unknown;
Expand All @@ -131,7 +131,7 @@ export function createInfiniteQueryOptions<
O,
ParamKey
>["getNextPageParam"];
queryKey: ConnectQueryKey;
queryKey: ConnectQueryKey<O>;
queryFn: SkipToken;
structuralSharing: (oldData: unknown, newData: unknown) => unknown;
initialPageParam: MessageInitShape<I>[ParamKey];
Expand All @@ -156,11 +156,11 @@ export function createInfiniteQueryOptions<
O,
ParamKey
>["getNextPageParam"];
queryKey: ConnectQueryKey;
queryKey: ConnectQueryKey<O>;
queryFn:
| QueryFunction<
MessageShape<O>,
ConnectQueryKey,
ConnectQueryKey<O>,
MessageInitShape<I>[ParamKey]
>
| SkipToken;
Expand All @@ -187,11 +187,11 @@ export function createInfiniteQueryOptions<
O,
ParamKey
>["getNextPageParam"];
queryKey: ConnectQueryKey;
queryKey: ConnectQueryKey<O>;
queryFn:
| QueryFunction<
MessageShape<O>,
ConnectQueryKey,
ConnectQueryKey<O>,
MessageInitShape<I>[ParamKey]
>
| SkipToken;
Expand Down
Loading