Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
06fb2d7
Initial work adapting new router and polymorphism approaches
Nick-Lucas Nov 14, 2024
a20fa7c
Switch around tests for better diffs
Nick-Lucas Nov 14, 2024
21b548d
Rename description just to avoid possible symbol conflicts
Nick-Lucas Nov 14, 2024
2f4a642
Comment back out sub-types tests as the types are borked
Nick-Lucas Nov 14, 2024
5057974
input
Nick-Lucas Nov 14, 2024
73a440f
In theory support query keys
Nick-Lucas Nov 14, 2024
6dea4c2
Fix query keys
Nick-Lucas Nov 14, 2024
039f1a4
Mutation never starts, tests are broken, wat
Nick-Lucas Nov 14, 2024
e0817fd
Fix Julius' terrible code :)
Nick-Lucas Nov 14, 2024
7ffa1d9
Support invalidate() function to compare with the more verbose approach
Nick-Lucas Nov 14, 2024
9d99878
Remove typecasts
Nick-Lucas Nov 15, 2024
da66346
Rename QueryKeyStuff
Nick-Lucas Nov 15, 2024
d99ad12
Add more API options for comparison
Nick-Lucas Nov 15, 2024
caac08f
Remove invalidate wrapper and finalise other functions
Nick-Lucas Nov 16, 2024
2a5e5b3
Move queryKey tests over and add queryFilter tests
Nick-Lucas Nov 16, 2024
da84aa9
Replace _input and _output with and with InferInput and InferOutput…
Nick-Lucas Nov 16, 2024
267c75c
Remove subtyped test and example because it probably cannot work and …
Nick-Lucas Nov 16, 2024
635922d
Remove RouterLike as it's redundant
Nick-Lucas Nov 16, 2024
0e31958
De-dup import
Nick-Lucas Nov 16, 2024
20bfef7
Update doc comments
Nick-Lucas Nov 16, 2024
68ed1e4
Update packages/tanstack-react-query/src/internals/createOptionsProxy.ts
Nick-Lucas Nov 17, 2024
397ed66
Fix bad commit
Nick-Lucas Nov 17, 2024
fa6811c
Move polymorphism tests into package
Nick-Lucas Nov 17, 2024
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
6 changes: 5 additions & 1 deletion packages/tanstack-react-query/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export { createTRPCContext } from './internals/Context';
export type { TRPCOptionsProxy } from './internals/createOptionsProxy';
export type {
TRPCOptionsProxy,
InferInput,
InferOutput,
} from './internals/createOptionsProxy';
export { createTRPCOptionsProxy } from './internals/createOptionsProxy';
export { useSubscription } from './internals/subscriptionOptions';
108 changes: 91 additions & 17 deletions packages/tanstack-react-query/src/internals/createOptionsProxy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type QueryClient } from '@tanstack/react-query';
import { type QueryClient, type QueryFilters } from '@tanstack/react-query';
import {
getUntypedClient,
TRPCUntypedClient,
Expand Down Expand Up @@ -33,10 +33,48 @@ import {
trpcSubscriptionOptions,
type TRPCSubscriptionOptions,
} from './subscriptionOptions';
import type { QueryType, ResolverDef } from './types';
import type { QueryType, ResolverDef, TRPCQueryKey } from './types';
import { getQueryKeyInternal } from './utils';

export interface DecorateQueryKeyable {
/**
* Calculate the Tanstack Query Key for a Route
*
* @see https://tanstack.com/query/latest/docs/framework/react/guides/query-keys
*/
queryKey: () => TRPCQueryKey;

/**
* Calculate a Tanstack Query Filter for a Route
*
* @see https://tanstack.com/query/latest/docs/framework/react/guides/filters
*/
queryFilter: () => QueryFilters;
Comment on lines +47 to +52

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still not convinced this is a very useful method, especially since it doesn't take any of the other query filter args (predicate functions etc). why not just use { queryKey: trpc.foo.queryKey() }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the idea would be that we have full control, so if we do want to set some of the other filters internally we can without breaking anybody's apps

I'm open to not having it, just felt like it's appropriate for a React Query factory to provide

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could also add options to override some bits, but I didn't want to dive straight into that world, worth analysing some use cases

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say until we have identified these use cases and added support for the other filter options just the queryKey suffices? Not a hill I'd die on so if you still see value in it go ahead and merge 👍

}

export type InferInput<
TProcedure extends
| DecorateQueryProcedure<any>
| DecorateMutationProcedure<any>,
> = TProcedure['~input'];

export type InferOutput<
TProcedure extends
| DecorateQueryProcedure<any>
| DecorateMutationProcedure<any>,
> = TProcedure['~output'];

export interface DecorateQueryProcedure<TDef extends ResolverDef> {
/**
* @internal prefer using InferInput to access input type
*/
'~input': TDef['input'];

/**
* @internal prefer using InferOutput to access input type
*/
'~output': TDef['output'];

/**
* @see https://tanstack.com/query/latest/docs/framework/react/reference/queryOptions#queryoptions
*/
Expand All @@ -46,9 +84,33 @@ export interface DecorateQueryProcedure<TDef extends ResolverDef> {
* @see https://tanstack.com/query/latest/docs/framework/react/reference/infiniteQueryOptions#infinitequeryoptions
*/
infiniteQueryOptions: TRPCInfiniteQueryOptions<TDef>;

/**
* Calculate the Tanstack Query Key for a Query Procedure
*
* @see https://tanstack.com/query/latest/docs/framework/react/guides/query-keys
*/
queryKey: (input?: TDef['input']) => TRPCQueryKey;

/**
* Calculate a Tanstack Query Filter for a Query Procedure
*
* @see https://tanstack.com/query/latest/docs/framework/react/guides/filters
*/
queryFilter: (input?: TDef['input']) => QueryFilters;
}

export interface DecorateMutationProcedure<TDef extends ResolverDef> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this have .mutationKey() ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's a different user story to queryKey really, I'm not very familiar with mutationKey so wouldn't want to rush into adding that without knowing the story, but it seems to be more about remotely executing a mutation which wouldn't make a lot of sense with tRPC

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's needed for queryClient.isMutating() for example, a utility we currently have

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, are you okay if we implement that as additional work into your branch? The scope of this PR already expanded a lot and it was all purely in service of polymorphism support. mutationKey isn't needed for that case

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes sure 👍 seems like this is targeting some other intermediate branch though 🤨

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait, what's the correct branch? I thought I had it from your PR?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is Alex's R19 branch which I think isn't working in some areas 🤔

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Crap. Well that threw me for a loop and into git hell: #6244

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Closing this in favour of the other MR

/**
* @internal prefer using InferInput to access input type
*/
'~input': TDef['input'];

/**
* @internal prefer using InferOutput to access input type
*/
'~output': TDef['output'];

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Putting the type on the object makes for a very cheap way of accessing type later, but providing some Infer helpers instead suggesting users utilise this directly. Prefixing with ~ forces autocomplete to sort it to the end

/**
* @see
*/
Expand Down Expand Up @@ -82,7 +144,7 @@ export type DecoratedProcedureUtilsRecord<
> = {
[TKey in keyof TRecord]: TRecord[TKey] extends infer $Value
? $Value extends RouterRecord
? DecoratedProcedureUtilsRecord<TRoot, $Value>
? DecoratedProcedureUtilsRecord<TRoot, $Value> & DecorateQueryKeyable
: $Value extends AnyProcedure
? DecorateProcedure<
$Value['_def']['type'],
Expand All @@ -101,7 +163,8 @@ export type TRPCOptionsProxy<TRouter extends AnyRouter> =
DecoratedProcedureUtilsRecord<
TRouter['_def']['_config']['$types'],
TRouter['_def']['record']
>;
> &
DecorateQueryKeyable;

export interface TRPCOptionsProxyOptionsBase {
queryClient: QueryClient;
Expand Down Expand Up @@ -132,12 +195,14 @@ type UtilsMethods =
| keyof DecorateSubscriptionProcedure<any>;

function getQueryType(method: UtilsMethods) {
return {
const map: Partial<Record<UtilsMethods, QueryType>> = {
queryOptions: 'query',
infiniteQueryOptions: 'infinite',
subscriptionOptions: 'any',
mutationOptions: 'any',
}[method] as QueryType;
};

return map[method];
}

export function createTRPCOptionsProxy<TRouter extends AnyRouter>(
Expand Down Expand Up @@ -172,23 +237,32 @@ export function createTRPCOptionsProxy<TRouter extends AnyRouter>(
return createRecursiveProxy(({ args, path: _path }) => {
const path = [..._path];
const utilName = path.pop() as UtilsMethods;
const [input, userOptions] = args as any[];
const [arg1, arg2] = args as any[];

const queryType = getQueryType(utilName);
const queryKey = getQueryKeyInternal(path, input, queryType);
const queryKey = getQueryKeyInternal(path, arg1, queryType ?? 'any');

const contextMap: Record<UtilsMethods, () => unknown> = {
infiniteQueryOptions: () =>
trpcInfiniteQueryOptions({
opts: userOptions,
'~input': undefined as any,
'~output': undefined as any,
queryKey: () => queryKey,
queryFilter: (): QueryFilters => {
return {
queryKey: queryKey,
Comment thread
Nick-Lucas marked this conversation as resolved.
};
},
infiniteQueryOptions: () => {
return trpcInfiniteQueryOptions({
opts: arg2,
path,
queryClient: opts.queryClient,
queryKey,
queryKey: queryKey,
query: callIt('query'),
}),
});
},
queryOptions: () => {
return trpcQueryOptions({
opts: userOptions,
opts: arg2,
path,
queryClient: opts.queryClient,
queryKey: queryKey,
Expand All @@ -197,7 +271,7 @@ export function createTRPCOptionsProxy<TRouter extends AnyRouter>(
},
mutationOptions: () => {
return trpcMutationOptions({
opts: userOptions,
opts: arg1,
path,
queryClient: opts.queryClient,
mutate: callIt('mutation'),
Expand All @@ -206,9 +280,9 @@ export function createTRPCOptionsProxy<TRouter extends AnyRouter>(
},
subscriptionOptions: () => {
return trpcSubscriptionOptions({
opts: userOptions,
opts: arg2,
path,
queryKey,
queryKey: queryKey,
subscribe: callIt('subscription'),
});
},
Expand Down
21 changes: 20 additions & 1 deletion packages/tanstack-react-query/test/mutationOptions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,26 @@ const ctx = konn()
describe('mutationOptions', () => {
test('useMutation', async () => {
const { App, useTRPC } = ctx;

const calls: string[] = [];

function MyComponent() {
const trpc = useTRPC();
const options = trpc.post.create.mutationOptions({});

const options = trpc.post.create.mutationOptions({
onMutate(variables) {
calls.push('onMutate');
},
onSettled(variables) {
calls.push('onSettled');
},
onError(variables) {
calls.push('onError');
},
onSuccess(variables) {
calls.push('onSuccess');
},
});
expect(options.trpc.path).toBe('post.create');

const mutation = useMutation(options);
Expand Down Expand Up @@ -86,5 +103,7 @@ describe('mutationOptions', () => {
await waitFor(() => {
expect(utils.container).toHaveTextContent(`__mutationResult`);
});

expect(calls).toEqual(['onMutate', 'onSuccess', 'onSettled']);
});
});
5 changes: 5 additions & 0 deletions packages/tanstack-react-query/test/polymorphism.common.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { initTRPC } from '@trpc/server';

export const t = initTRPC.create();

export type $RootTypes = (typeof t)['_config']['$types'];
104 changes: 104 additions & 0 deletions packages/tanstack-react-query/test/polymorphism.factory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//
// This file contains a useful pattern in tRPC,
// building factories which can produce common functionality over a homologous data source.
//
import { TRPCError } from '@trpc/server';
import type {
AnyRootTypes,
createBuilder,
} from '@trpc/server/unstable-core-do-not-import';
import z from 'zod';
import type { TRPCOptionsProxy } from '../src';
import type { $RootTypes } from './polymorphism.common';
import { t } from './polymorphism.common';

//
// DTOs
//

export const FileExportRequest = z.object({
name: z.string().min(0),
filter: z.string().min(0),
});

export const FileExportStatus = z.object({
id: z.number().min(0),
name: z.string().min(0),
downloadUri: z.string().optional(),
createdAt: z.date(),
});
export type FileExportStatusType = z.infer<typeof FileExportStatus>;

//
// Dependencies
//

type BaseProcedure<TRoot extends AnyRootTypes> = ReturnType<
typeof createBuilder<TRoot['ctx'], TRoot['meta']>
>;

export type DataProvider = FileExportStatusType[];

//
// Set up a route factory which can be re-used for different data sources.
// In this case just with a simple array data source a POC
//

let COUNTER = 1;

export function createExportRoute<
TBaseProcedure extends BaseProcedure<$RootTypes>,
>(baseProcedure: TBaseProcedure, dataProvider: DataProvider) {
return t.router({
start: baseProcedure
.input(FileExportRequest)
.output(FileExportStatus)
.mutation(async (opts) => {
const exportInstance: FileExportStatusType = {
id: COUNTER++,
name: opts.input.name,
createdAt: new Date(),
downloadUri: undefined,
};

dataProvider.push(exportInstance);

return exportInstance;
}),
list: baseProcedure.output(z.array(FileExportStatus)).query(async () => {
return dataProvider;
}),
status: baseProcedure
.input(z.object({ id: z.number().min(0) }))
.output(FileExportStatus)
.query(async (opts) => {
const index = dataProvider.findIndex(
(item) => item.id === opts.input.id,
);

const exportInstance = dataProvider[index];

if (!exportInstance) {
throw new TRPCError({
code: 'NOT_FOUND',
});
}

// When status is polled a second time the download should be ready
dataProvider[index] = {
...exportInstance,
downloadUri: `example.com/export-${exportInstance.name}.csv`,
};

return exportInstance;
}),
});
}

//
// Generate abstract types which can be used by the client
//

type ExportRouteType = ReturnType<typeof createExportRoute>;

export type ExportRouteLike = TRPCOptionsProxy<ExportRouteType>;
Loading