From bad022fc49fa82c2fe929deef9003a2b59cde294 Mon Sep 17 00:00:00 2001 From: unnoq Date: Sun, 29 Dec 2024 15:31:45 +0700 Subject: [PATCH] sync @orpc/react-query --- packages/react-query/src/types.ts | 14 ++- .../react-query/src/utils-procedure.test-d.ts | 101 +++++++++++++++--- .../react-query/src/utils-procedure.test.ts | 63 ++++++++++- packages/react-query/src/utils-procedure.ts | 30 +++--- .../react-query/src/utils-router.test-d.ts | 22 +++- packages/react-query/src/utils-router.ts | 10 +- packages/react-query/tests/e2e.test-d.ts | 52 +++++++++ packages/react-query/tests/helpers.tsx | 13 ++- 8 files changed, 257 insertions(+), 48 deletions(-) diff --git a/packages/react-query/src/types.ts b/packages/react-query/src/types.ts index d103ff6c..9c56609a 100644 --- a/packages/react-query/src/types.ts +++ b/packages/react-query/src/types.ts @@ -9,15 +9,23 @@ import type { export type InferCursor = T extends { cursor?: any } ? T['cursor'] : never -export type QueryOptions = (undefined extends TInput ? { input?: TInput } : { input: TInput }) & ( +export type QueryOptions = +& (undefined extends TInput ? { input?: TInput } : { input: TInput }) +& (undefined extends TClientContext ? { context?: TClientContext } : { context: TClientContext }) +& ( SetOptional, 'queryKey'> ) -export type InfiniteOptions = (undefined extends TInput ? { input?: Omit } : { input: Omit }) & ( +export type InfiniteOptions = +& (undefined extends TInput ? { input?: Omit } : { input: Omit }) +& (undefined extends TClientContext ? { context?: TClientContext } : { context: TClientContext }) +& ( SetOptional< UndefinedInitialDataInfiniteOptions>, 'queryKey' | (undefined extends InferCursor ? 'initialPageParam' : never) > ) -export type MutationOptions = UseMutationOptions +export type MutationOptions = + & (undefined extends TClientContext ? { context?: TClientContext } : { context: TClientContext }) + & UseMutationOptions diff --git a/packages/react-query/src/utils-procedure.test-d.ts b/packages/react-query/src/utils-procedure.test-d.ts index dea2bbf0..a055a99e 100644 --- a/packages/react-query/src/utils-procedure.test-d.ts +++ b/packages/react-query/src/utils-procedure.test-d.ts @@ -1,15 +1,16 @@ import type { ProcedureClient } from '@orpc/server' import type { InfiniteData, QueryKey } from '@tanstack/react-query' -import { useInfiniteQuery, useQuery } from '@tanstack/react-query' +import type { ProcedureUtils } from './utils-procedure' +import { useInfiniteQuery, useMutation, useQuery } from '@tanstack/react-query' import { createProcedureUtils } from './utils-procedure' describe('queryOptions', () => { - const client = vi.fn>( + const client = vi.fn>( (...[input]) => Promise.resolve(input?.toString()), ) const utils = createProcedureUtils(client, []) - const client2 = vi.fn((input: number) => Promise.resolve(input.toString())) + const client2 = {} as ProcedureClient const utils2 = createProcedureUtils(client2, []) it('infer correct input type', () => { @@ -51,13 +52,36 @@ describe('queryOptions', () => { expectTypeOf(query.data).toEqualTypeOf<12344>() } }) + + describe('client context', () => { + it('can be optional', () => { + const utils = {} as ProcedureUtils + useQuery(utils.queryOptions()) + useQuery(utils.queryOptions({})) + useQuery(utils.queryOptions({ context: undefined })) + useQuery(utils.queryOptions({ context: { batch: true } })) + // @ts-expect-error --- invalid context + useQuery(utils.queryOptions({ context: { batch: 'invalid' } })) + }) + + it('required pass context when non-optional', () => { + const utils = {} as ProcedureUtils + // @ts-expect-error --- missing context + useQuery(utils.queryOptions()) + // @ts-expect-error --- missing context + useQuery(utils.queryOptions({})) + useQuery(utils.queryOptions({ context: { batch: true } })) + // @ts-expect-error --- invalid context + useQuery(utils.queryOptions({ context: { batch: 123 } })) + }) + }) }) describe('infiniteOptions', () => { const getNextPageParam = vi.fn() it('cannot use on procedure without input object-able', () => { - const utils = createProcedureUtils({} as (input: number) => Promise, []) + const utils = createProcedureUtils({} as ProcedureClient, []) // @ts-expect-error missing initialPageParam utils.infiniteOptions({ @@ -80,7 +104,7 @@ describe('infiniteOptions', () => { }) it('infer correct input type', () => { - const utils = createProcedureUtils({} as (input: { limit?: number, cursor: number }) => Promise, []) + const utils = createProcedureUtils({} as ProcedureClient<{ limit?: number, cursor: number }, string, unknown>, []) utils.infiniteOptions({ input: { @@ -111,7 +135,7 @@ describe('infiniteOptions', () => { }) it('infer correct initialPageParam type', () => { - const utils = createProcedureUtils({} as (input: { limit?: number, cursor: number }) => Promise, []) + const utils = createProcedureUtils({} as ProcedureClient<{ limit?: number, cursor: number }, string, unknown>, []) utils.infiniteOptions({ input: {}, @@ -134,14 +158,14 @@ describe('infiniteOptions', () => { }) it('initialPageParam can be optional', () => { - const utils = createProcedureUtils({} as (input: { limit?: number, cursor?: number }) => Promise, []) + const utils = createProcedureUtils({} as ProcedureClient<{ limit?: number, cursor?: number }, string, unknown>, []) utils.infiniteOptions({ input: {}, getNextPageParam, }) - const utils2 = createProcedureUtils({} as (input: { limit?: number, cursor: number }) => Promise, []) + const utils2 = createProcedureUtils({} as ProcedureClient<{ limit?: number, cursor: number }, string, unknown>, []) // @ts-expect-error initialPageParam is required utils2.infiniteOptions({ @@ -151,13 +175,13 @@ describe('infiniteOptions', () => { }) it('input can be optional', () => { - const utils = createProcedureUtils({} as ProcedureClient<{ limit?: number, cursor?: number } | undefined, string>, []) + const utils = createProcedureUtils({} as ProcedureClient<{ limit?: number, cursor?: number } | undefined, string, unknown>, []) utils.infiniteOptions({ getNextPageParam, }) - const utils2 = createProcedureUtils({} as (input: { limit?: number, cursor?: number }) => Promise, []) + const utils2 = createProcedureUtils({} as ProcedureClient<{ limit?: number, cursor?: number }, string, unknown>, []) // @ts-expect-error input is required utils2.infiniteOptions({ @@ -166,7 +190,7 @@ describe('infiniteOptions', () => { }) it('infer correct output type', () => { - const utils = createProcedureUtils({} as (input: { limit?: number, cursor: number }) => Promise, []) + const utils = createProcedureUtils({} as ProcedureClient<{ limit?: number, cursor: number }, string, unknown>, []) const query = useInfiniteQuery(utils.infiniteOptions({ input: { limit: 1, @@ -181,7 +205,7 @@ describe('infiniteOptions', () => { }) it('work with select options', () => { - const utils = createProcedureUtils({} as (input: { limit?: number, cursor: number }) => Promise, []) + const utils = createProcedureUtils({} as ProcedureClient<{ limit?: number, cursor: number }, string, unknown>, []) const query = useInfiniteQuery(utils.infiniteOptions({ input: { limit: 1, @@ -199,10 +223,38 @@ describe('infiniteOptions', () => { expectTypeOf(query.data).toEqualTypeOf<{ value: string }>() } }) + + describe('client context', () => { + it('can be optional', () => { + const utils = {} as ProcedureUtils + + const getNextPageParam = vi.fn() + const initialPageParam = 1 + + useInfiniteQuery(utils.infiniteOptions({ getNextPageParam, initialPageParam })) + useInfiniteQuery(utils.infiniteOptions({ getNextPageParam, initialPageParam, context: undefined })) + useInfiniteQuery(utils.infiniteOptions({ getNextPageParam, initialPageParam, context: { batch: true } })) + // @ts-expect-error --- invalid context + useInfiniteQuery(utils.infiniteOptions({ getNextPageParam, initialPageParam, context: { batch: 'invalid' } })) + }) + + it('required pass context when non-optional', () => { + const utils = {} as ProcedureUtils + + const getNextPageParam = vi.fn() + const initialPageParam = 1 + + // @ts-expect-error --- missing context + useInfiniteQuery(utils.infiniteOptions({ getNextPageParam, initialPageParam })) + useInfiniteQuery(utils.infiniteOptions({ getNextPageParam, initialPageParam, context: { batch: true } })) + // @ts-expect-error --- invalid context + useInfiniteQuery(utils.infiniteOptions({ getNextPageParam, initialPageParam, context: { batch: 'invalid' } })) + }) + }) }) describe('mutationOptions', () => { - const client = vi.fn((input: number) => Promise.resolve(input.toString())) + const client = {} as ProcedureClient const utils = createProcedureUtils(client, []) it('infer correct input type', () => { @@ -237,4 +289,27 @@ describe('mutationOptions', () => { expectTypeOf(option.mutationKey).toEqualTypeOf() expectTypeOf(option.mutationFn).toEqualTypeOf<(input: number) => Promise>() }) + + describe('client context', () => { + it('can be optional', () => { + const utils = {} as ProcedureUtils + useMutation(utils.mutationOptions()) + useMutation(utils.mutationOptions({})) + useMutation(utils.mutationOptions({ context: undefined })) + useMutation(utils.mutationOptions({ context: { batch: true } })) + // @ts-expect-error --- invalid context + useMutation(utils.mutationOptions({ context: { batch: 'invalid' } })) + }) + + it('required pass context when non-optional', () => { + const utils = {} as ProcedureUtils + // @ts-expect-error --- missing context + useMutation(utils.mutationOptions()) + // @ts-expect-error --- missing context + useMutation(utils.mutationOptions({})) + useMutation(utils.mutationOptions({ context: { batch: true } })) + // @ts-expect-error --- invalid context + useMutation(utils.mutationOptions({ context: { batch: 123 } })) + }) + }) }) diff --git a/packages/react-query/src/utils-procedure.test.ts b/packages/react-query/src/utils-procedure.test.ts index 23ab5fd9..82b19f8c 100644 --- a/packages/react-query/src/utils-procedure.test.ts +++ b/packages/react-query/src/utils-procedure.test.ts @@ -1,4 +1,3 @@ -import type { ProcedureClient } from '@orpc/server' import * as keyModule from './key' import { createProcedureUtils } from './utils-procedure' @@ -12,7 +11,7 @@ beforeEach(() => { }) describe('queryOptions', () => { - const client = vi.fn>((...[input]) => Promise.resolve(input?.toString())) + const client = vi.fn((...[input]) => Promise.resolve(input?.toString())) const utils = createProcedureUtils(client, ['ping']) it('works', async () => { @@ -27,13 +26,29 @@ describe('queryOptions', () => { expect(client).toHaveBeenCalledTimes(1) expect(client).toBeCalledWith(1, { signal }) }) + + it('works with client context', async () => { + const client = vi.fn((...[input]) => Promise.resolve(input?.toString())) + const utils = createProcedureUtils(client, ['ping']) + + const options = utils.queryOptions({ context: { batch: true } }) + + expect(options.queryKey).toEqual(['__ORPC__', ['ping'], { type: 'query' }]) + expect(buildKeySpy).toHaveBeenCalledTimes(1) + expect(buildKeySpy).toHaveBeenCalledWith(['ping'], { type: 'query' }) + + client.mockResolvedValueOnce('__mocked__') + await expect((options as any).queryFn({ signal })).resolves.toEqual('__mocked__') + expect(client).toHaveBeenCalledTimes(1) + expect(client).toBeCalledWith(undefined, { signal, context: { batch: true } }) + }) }) describe('infiniteOptions', () => { const getNextPageParam = vi.fn() it('works ', async () => { - const client = vi.fn<(input: { limit?: number, cursor: number | undefined }) => Promise>() + const client = vi.fn() const utils = createProcedureUtils(client, []) const options = utils.infiniteOptions({ @@ -54,7 +69,7 @@ describe('infiniteOptions', () => { }) it('works without initialPageParam', async () => { - const client = vi.fn<(input: { limit?: number, cursor: number | undefined }) => Promise>() + const client = vi.fn() const utils = createProcedureUtils(client, []) const options = utils.infiniteOptions({ @@ -71,11 +86,31 @@ describe('infiniteOptions', () => { expect(client).toHaveBeenCalledTimes(1) expect(client).toBeCalledWith({ limit: 5, cursor: undefined }, { signal }) }) + + it('works with client context', async () => { + const client = vi.fn() + const utils = createProcedureUtils(client, []) + + const options = utils.infiniteOptions({ + context: { batch: true }, + getNextPageParam, + initialPageParam: 1, + }) + + expect(options.queryKey).toEqual(['__ORPC__', [], { type: 'infinite' }]) + expect(buildKeySpy).toHaveBeenCalledTimes(1) + expect(buildKeySpy).toHaveBeenCalledWith([], { type: 'infinite' }) + + client.mockResolvedValueOnce('__mocked__') + await expect((options as any).queryFn({ pageParam: 1, signal })).resolves.toEqual('__mocked__') + expect(client).toHaveBeenCalledTimes(1) + expect(client).toBeCalledWith({ limit: undefined, cursor: 1 }, { signal, context: { batch: true } }) + }) }) describe('mutationOptions', () => { it('works', async () => { - const client = vi.fn>( + const client = vi.fn( (...[input]) => Promise.resolve(input?.toString()), ) const utils = createProcedureUtils(client, ['ping']) @@ -91,4 +126,22 @@ describe('mutationOptions', () => { expect(client).toHaveBeenCalledTimes(1) expect(client).toBeCalledWith(1) }) + + it('works with client context', async () => { + const client = vi.fn( + (...[input]) => Promise.resolve(input?.toString()), + ) + const utils = createProcedureUtils(client, ['ping']) + + const options = utils.mutationOptions({ context: { batch: true } }) + + expect(options.mutationKey).toEqual(['__ORPC__', ['ping'], { type: 'mutation' }]) + expect(buildKeySpy).toHaveBeenCalledTimes(1) + expect(buildKeySpy).toHaveBeenCalledWith(['ping'], { type: 'mutation' }) + + client.mockResolvedValueOnce('__mocked__') + await expect(options.mutationFn(1)).resolves.toEqual('__mocked__') + expect(client).toHaveBeenCalledTimes(1) + expect(client).toBeCalledWith(1, { context: { batch: true } }) + }) }) diff --git a/packages/react-query/src/utils-procedure.ts b/packages/react-query/src/utils-procedure.ts index 6bf69a22..d98875f4 100644 --- a/packages/react-query/src/utils-procedure.ts +++ b/packages/react-query/src/utils-procedure.ts @@ -7,35 +7,35 @@ import { buildKey } from './key' /** * Utils at procedure level */ -export interface ProcedureUtils { - queryOptions: >( - ...opts: [options: U] | (undefined extends TInput ? [] : never) - ) => IsEqual> extends true +export interface ProcedureUtils { + queryOptions: >( + ...opts: [options: U] | (undefined extends TInput & TClientContext ? [] : never) + ) => IsEqual> extends true ? { queryKey: QueryKey, queryFn: () => Promise } : Omit<{ queryKey: QueryKey, queryFn: () => Promise }, keyof U> & U - infiniteOptions: >( + infiniteOptions: >( options: U ) => Omit<{ queryKey: QueryKey, queryFn: () => Promise, initialPageParam: undefined }, keyof U> & U - mutationOptions: >( - options?: U - ) => IsEqual> extends true + mutationOptions: >( + ...opt: [options: U] | (undefined extends TClientContext ? [] : never) + ) => IsEqual> extends true ? { mutationKey: QueryKey, mutationFn: (input: TInput) => Promise } : Omit<{ mutationKey: QueryKey, mutationFn: (input: TInput) => Promise }, keyof U> & U } -export function createProcedureUtils( - client: ProcedureClient, +export function createProcedureUtils( + client: ProcedureClient, path: string[], -): ProcedureUtils { +): ProcedureUtils { return { queryOptions(...[options]) { const input = options?.input as any return { queryKey: buildKey(path, { type: 'query', input }), - queryFn: ({ signal }) => client(input, { signal }), + queryFn: ({ signal }) => client(input, { signal, context: options?.context } as any), ...(options as any), } }, @@ -45,15 +45,15 @@ export function createProcedureUtils( return { queryKey: buildKey(path, { type: 'infinite', input }), - queryFn: ({ pageParam, signal }) => client({ ...input, cursor: pageParam }, { signal }), + queryFn: ({ pageParam, signal }) => client({ ...input, cursor: pageParam }, { signal, context: options.context } as any), ...(options as any), } }, - mutationOptions(options) { + mutationOptions(...[options]) { return { mutationKey: buildKey(path, { type: 'mutation' }), - mutationFn: input => client(input), + mutationFn: input => client(input, { context: options?.context } as any), ...(options as any), } }, diff --git a/packages/react-query/src/utils-router.test-d.ts b/packages/react-query/src/utils-router.test-d.ts index 488ec9b7..be71ea49 100644 --- a/packages/react-query/src/utils-router.test-d.ts +++ b/packages/react-query/src/utils-router.test-d.ts @@ -1,4 +1,6 @@ import type { RouterClient } from '@orpc/server' +import type { GeneralUtils } from './utils-general' +import type { ProcedureUtils } from './utils-procedure' import { oc } from '@orpc/contract' import { os } from '@orpc/server' import { z } from 'zod' @@ -23,7 +25,7 @@ const router = os.contract(contractRouter).router({ describe('with contract router', () => { it('build correct types', () => { - const utils = createRouterUtils({} as RouterClient) + const utils = createRouterUtils({} as RouterClient) const generalUtils = createGeneralUtils([]) const pingUtils = createProcedureUtils(ping, []) @@ -41,7 +43,7 @@ describe('with contract router', () => { describe('with router', () => { it('build correct types', () => { - const utils = createRouterUtils({} as RouterClient) + const utils = createRouterUtils({} as RouterClient) const generalUtils = createGeneralUtils([]) const pingUtils = createProcedureUtils(ping, []) @@ -56,3 +58,19 @@ describe('with router', () => { expectTypeOf(utils.pong).toMatchTypeOf() }) }) + +it('with client context', () => { + const utils = createRouterUtils({} as RouterClient) + + const generalUtils = {} as GeneralUtils + const pingUtils = {} as ProcedureUtils<{ name: string }, string, undefined | { batch?: boolean }> + const pingGeneralUtils = createGeneralUtils<{ name: string }>(['ping']) + const pongUtils = {} as ProcedureUtils + const pongGeneralUtils = {} as GeneralUtils + + expectTypeOf(utils).toMatchTypeOf() + expectTypeOf(utils.ping).toMatchTypeOf() + expectTypeOf(utils.ping).toMatchTypeOf() + expectTypeOf(utils.pong).toMatchTypeOf() + expectTypeOf(utils.pong).toMatchTypeOf() +}) diff --git a/packages/react-query/src/utils-router.ts b/packages/react-query/src/utils-router.ts index b352410b..30acc3ea 100644 --- a/packages/react-query/src/utils-router.ts +++ b/packages/react-query/src/utils-router.ts @@ -2,18 +2,18 @@ import type { ProcedureClient, RouterClient } from '@orpc/server' import { createGeneralUtils, type GeneralUtils } from './utils-general' import { createProcedureUtils, type ProcedureUtils } from './utils-procedure' -export type RouterUtils> = -T extends ProcedureClient - ? ProcedureUtils & GeneralUtils +export type RouterUtils> = +T extends ProcedureClient + ? ProcedureUtils & GeneralUtils : { - [K in keyof T]: T[K] extends RouterClient ? RouterUtils : never + [K in keyof T]: T[K] extends RouterClient ? RouterUtils : never } & GeneralUtils /** * @param client - The client create form `@orpc/client` * @param path - The base path for query key */ -export function createRouterUtils>( +export function createRouterUtils>( client: T, path: string[] = [], ): RouterUtils { diff --git a/packages/react-query/tests/e2e.test-d.ts b/packages/react-query/tests/e2e.test-d.ts index 50f2971f..15a7f151 100644 --- a/packages/react-query/tests/e2e.test-d.ts +++ b/packages/react-query/tests/e2e.test-d.ts @@ -28,6 +28,13 @@ describe('useQuery', () => { // @ts-expect-error input is invalid useQuery(orpc.user.find.queryOptions({ input: { id: 123 } })) }) + + it('infer types correctly with client context', async () => { + useQuery(orpc.user.find.queryOptions({ input: { id: '123' } })) + useQuery(orpc.user.find.queryOptions({ input: { id: '123' }, context: { batch: true } })) + // @ts-expect-error --- invalid context + useQuery(orpc.user.find.queryOptions({ input: { id: '123' }, context: { batch: 'invalid' } })) + }) }) describe('useInfiniteQuery', () => { @@ -84,6 +91,25 @@ describe('useInfiniteQuery', () => { getNextPageParam: {} as any, })) }) + + it('infer types correctly with client context', async () => { + useInfiniteQuery(orpc.user.list.infiniteOptions({ + input: { keyword: 'keyword' }, + getNextPageParam: {} as any, + })) + useInfiniteQuery(orpc.user.list.infiniteOptions({ + input: { keyword: 'keyword' }, + getNextPageParam: {} as any, + context: { batch: true }, + })) + // @ts-expect-error --- invalid context + useInfiniteQuery(orpc.user.list.infiniteOptions({ + input: { keyword: 'keyword' }, + getNextPageParam: {} as any, + // @ts-expect-error --- invalid context + context: { batch: 'invalid' }, + })) + }) }) describe('useMutation', () => { @@ -98,6 +124,13 @@ describe('useMutation', () => { expectTypeOf(query.mutateAsync).toMatchTypeOf<(input: { id: string }) => Promise<{ id: string, name: string }>>() }) + + it('infer types correctly with client context', async () => { + useMutation(orpc.user.find.mutationOptions(({}))) + useMutation(orpc.user.find.mutationOptions(({ context: { batch: true } }))) + // @ts-expect-error --- invalid context + useMutation(orpc.user.find.mutationOptions(({ context: { batch: 'invalid' } }))) + }) }) describe('useQueries', () => { @@ -163,4 +196,23 @@ describe('useQueries', () => { ], }) }) + + it('infer types correctly with client context', async () => { + useQueries({ + queries: [ + orpc.user.find.queryOptions({ + input: { id: '0' }, + }), + orpc.user.find.queryOptions({ + input: { id: '0' }, + context: { batch: true }, + }), + orpc.user.find.queryOptions({ + input: { id: '0' }, + // @ts-expect-error --- invalid context + context: { batch: 'invalid' }, + }), + ], + }) + }) }) diff --git a/packages/react-query/tests/helpers.tsx b/packages/react-query/tests/helpers.tsx index 3214cce3..1dd20c57 100644 --- a/packages/react-query/tests/helpers.tsx +++ b/packages/react-query/tests/helpers.tsx @@ -1,4 +1,5 @@ -import { createORPCFetchClient } from '@orpc/client' +import { createClient } from '@orpc/client' +import { ORPCLink } from '@orpc/client/fetch' import { os } from '@orpc/server' import { ORPCHandler } from '@orpc/server/fetch' import { QueryClient } from '@tanstack/react-query' @@ -93,17 +94,19 @@ export const appRouter = orpcServer.router({ const orpcHandler = new ORPCHandler(appRouter) -export const orpcClient = createORPCFetchClient({ - baseURL: 'http://localhost:3000', +const orpcLink = new ORPCLink({ + url: 'http://localhost:3000', - async fetch(...args) { + async fetch(input, init) { await new Promise(resolve => setTimeout(resolve, 100)) - const request = new Request(...args) + const request = new Request(input, init) return orpcHandler.fetch(request) }, }) +export const orpcClient = createClient(orpcLink) + export const orpc = createORPCReactQueryUtils(orpcClient) export const queryClient = new QueryClient({