From 8d9c26c98f8770e45bdb3cdaabcacec69a142141 Mon Sep 17 00:00:00 2001 From: unnoq Date: Mon, 9 Dec 2024 08:53:33 +0700 Subject: [PATCH 01/17] init --- packages/react-query/.gitignore | 26 +++++++++++++++++ packages/react-query/package.json | 47 ++++++++++++++++++++++++++++++ packages/react-query/src/index.ts | 3 ++ packages/react-query/tsconfig.json | 15 ++++++++++ pnpm-lock.yaml | 2 ++ 5 files changed, 93 insertions(+) create mode 100644 packages/react-query/.gitignore create mode 100644 packages/react-query/package.json create mode 100644 packages/react-query/src/index.ts create mode 100644 packages/react-query/tsconfig.json diff --git a/packages/react-query/.gitignore b/packages/react-query/.gitignore new file mode 100644 index 00000000..f3620b55 --- /dev/null +++ b/packages/react-query/.gitignore @@ -0,0 +1,26 @@ +# Hidden folders and files +.* +!.gitignore +!.*.example + +# Common generated folders +logs/ +node_modules/ +out/ +dist/ +dist-ssr/ +build/ +coverage/ +temp/ + +# Common generated files +*.log +*.log.* +*.tsbuildinfo +*.vitest-temp.json +vite.config.ts.timestamp-* +vitest.config.ts.timestamp-* + +# Common manual ignore files +*.local +*.pem \ No newline at end of file diff --git a/packages/react-query/package.json b/packages/react-query/package.json new file mode 100644 index 00000000..567aff6e --- /dev/null +++ b/packages/react-query/package.json @@ -0,0 +1,47 @@ +{ + "name": "@orpc/react-query", + "type": "module", + "version": "0.0.0", + "license": "MIT", + "homepage": "https://orpc.unnoq.com", + "repository": { + "type": "git", + "url": "git+https://github.com/unnoq/orpc.git", + "directory": "packages/react-query" + }, + "keywords": [ + "unnoq", + "orpc", + "react-query", + "tanstack query", + "react" + ], + "publishConfig": { + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./🔒/*": { + "types": "./dist/src/*.d.ts" + } + } + }, + "exports": { + ".": "./src/index.ts", + "./🔒/*": { + "types": "./src/*.ts" + } + }, + "files": [ + "!**/*.map", + "!**/*.tsbuildinfo", + "dist" + ], + "scripts": { + "build": "tsup --clean --sourcemap --entry.index=src/index.ts --format=esm --onSuccess='tsc -b --noCheck'", + "build:watch": "pnpm run build --watch", + "type:check": "tsc -b" + } +} diff --git a/packages/react-query/src/index.ts b/packages/react-query/src/index.ts new file mode 100644 index 00000000..2c2d96ee --- /dev/null +++ b/packages/react-query/src/index.ts @@ -0,0 +1,3 @@ +/** unnoq */ + +export const author = 'unnoq' diff --git a/packages/react-query/tsconfig.json b/packages/react-query/tsconfig.json new file mode 100644 index 00000000..b64920aa --- /dev/null +++ b/packages/react-query/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.lib.json", + "compilerOptions": { + "types": [] + }, + "references": [], + "include": ["src"], + "exclude": [ + "**/*.test.*", + "**/*.test-d.ts", + "**/__tests__/**", + "**/__mocks__/**", + "**/__snapshots__/**" + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 71303044..f1615336 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -260,6 +260,8 @@ importers: specifier: ^3.23.8 version: 3.23.8 + packages/react-query: {} + packages/server: dependencies: '@orpc/contract': From 0c8d658caa9315fa88a3f917aae3f755cdef6f66 Mon Sep 17 00:00:00 2001 From: unnoq Date: Mon, 9 Dec 2024 13:48:24 +0700 Subject: [PATCH 02/17] wip --- packages/react-query/package.json | 10 +++ packages/react-query/src/index.ts | 10 ++- packages/react-query/src/key.ts | 27 +++++++++ packages/react-query/src/types.ts | 20 ++++++ packages/react-query/src/utils-general.ts | 20 ++++++ packages/react-query/src/utils-procedure.ts | 67 +++++++++++++++++++++ packages/react-query/src/utils-router.ts | 59 ++++++++++++++++++ packages/react-query/tsconfig.json | 7 ++- pnpm-lock.yaml | 21 ++++++- 9 files changed, 237 insertions(+), 4 deletions(-) create mode 100644 packages/react-query/src/key.ts create mode 100644 packages/react-query/src/types.ts create mode 100644 packages/react-query/src/utils-general.ts create mode 100644 packages/react-query/src/utils-procedure.ts create mode 100644 packages/react-query/src/utils-router.ts diff --git a/packages/react-query/package.json b/packages/react-query/package.json index 567aff6e..7527c39e 100644 --- a/packages/react-query/package.json +++ b/packages/react-query/package.json @@ -43,5 +43,15 @@ "build": "tsup --clean --sourcemap --entry.index=src/index.ts --format=esm --onSuccess='tsc -b --noCheck'", "build:watch": "pnpm run build --watch", "type:check": "tsc -b" + }, + "peerDependencies": { + "@orpc/client": "workspace:*", + "@orpc/contract": "workspace:*", + "@orpc/server": "workspace:*", + "@tanstack/react-query": ">=5.59.0", + "react": ">=18.3.0" + }, + "dependencies": { + "@orpc/shared": "workspace:*" } } diff --git a/packages/react-query/src/index.ts b/packages/react-query/src/index.ts index 2c2d96ee..e8ab418b 100644 --- a/packages/react-query/src/index.ts +++ b/packages/react-query/src/index.ts @@ -1,3 +1,9 @@ -/** unnoq */ +import { createRouterUtils } from './utils-router' -export const author = 'unnoq' +export * from './key' +export * from './types' +export * from './utils-general' +export * from './utils-procedure' +export * from './utils-router' + +export const createORPCReactQueryUtils = createRouterUtils diff --git a/packages/react-query/src/key.ts b/packages/react-query/src/key.ts new file mode 100644 index 00000000..951d4456 --- /dev/null +++ b/packages/react-query/src/key.ts @@ -0,0 +1,27 @@ +import type { QueryKey } from '@tanstack/react-query' + +export type KeyType = 'query' | 'infinite' | 'mutation' | undefined + +export interface BuildKeyOptions { + type?: TType + input?: TType extends 'mutation' ? never : TInput +} + +export function buildKey( + prefix: string, + path: string[], + options?: BuildKeyOptions, +): QueryKey { + const withInput + = options?.input !== undefined ? { input: options?.input } : {} + const withType = options?.type !== undefined ? { type: options?.type } : {} + + return [ + prefix, + path, + { + ...withInput, + ...withType, + }, + ] +} diff --git a/packages/react-query/src/types.ts b/packages/react-query/src/types.ts new file mode 100644 index 00000000..7bd9c425 --- /dev/null +++ b/packages/react-query/src/types.ts @@ -0,0 +1,20 @@ +import type { + DefaultError, + QueryKey, + UseInfiniteQueryOptions, + UseMutationOptions, + UseQueryOptions, +} from '@tanstack/react-query' + +export interface QueryOptions extends UseQueryOptions { + input: TInput +} + +export interface InfiniteOptions extends UseInfiniteQueryOptions { + input: TInput +} + +export interface MutationOptions extends UseMutationOptions { +} + +export type InferCursor = T extends { cursor?: any } ? T['cursor'] : never diff --git a/packages/react-query/src/utils-general.ts b/packages/react-query/src/utils-general.ts new file mode 100644 index 00000000..f711642b --- /dev/null +++ b/packages/react-query/src/utils-general.ts @@ -0,0 +1,20 @@ +import type { QueryKey } from '@tanstack/react-query' +import { buildKey, type BuildKeyOptions } from './key' + +/** + * Utils at any level (procedure or router) + */ +export interface GeneralUtils { + key: (options?: BuildKeyOptions) => QueryKey +} + +export function createGeneralUtils( + prefix: string, + path: string[], +): GeneralUtils { + return { + key(options) { + return buildKey(prefix, path, options) + }, + } +} diff --git a/packages/react-query/src/utils-procedure.ts b/packages/react-query/src/utils-procedure.ts new file mode 100644 index 00000000..21ad3fb9 --- /dev/null +++ b/packages/react-query/src/utils-procedure.ts @@ -0,0 +1,67 @@ +import type { PartialOnUndefinedDeep, SetOptional } from '@orpc/shared' +import type { DefaultError, InfiniteData, QueryKey, UseInfiniteQueryOptions, UseMutationOptions, UseQueryOptions } from '@tanstack/react-query' +import type { InferCursor } from './types' +import { buildKey } from './key' + +/** + * Utils at procedure level + */ +export interface ProcedureUtils { + queryOptions: ( + options: + & SetOptional, 'queryFn' | 'queryKey'> + & PartialOnUndefinedDeep<{ input: TInput }> + ) => UseQueryOptions + + infiniteOptions: >>( + options: + & SetOptional< + PartialOnUndefinedDeep< + UseInfiniteQueryOptions> + >, + 'queryFn' | 'queryKey' + > + & PartialOnUndefinedDeep<{ input: Omit }> + ) => UseInfiniteQueryOptions> + + mutationOptions: ( + options: SetOptional, 'mutationFn' | 'mutationKey'> + ) => UseMutationOptions +} + +export function createProcedureUtils( + prefix: string, + client: (input: TInput) => Promise, +): ProcedureUtils { + const path: string[] = [] // TODO + + return { + queryOptions(options) { + const input = options.input as TInput + + return { + queryKey: buildKey(prefix, path, { type: 'query', input }), + queryFn: () => client(input), + ...options, + } + }, + + infiniteOptions(options) { + const input = options.input as Omit + + return { + queryKey: buildKey(prefix, path, { type: 'infinite', input }), + queryFn: ({ pageParam }) => client({ ...(input as any), cursor: pageParam }), + ...(options as any), + } + }, + + mutationOptions(options) { + return { + mutationKey: buildKey(prefix, path, { type: 'mutation' }), + mutationFn: input => client(input), + ...options, + } + }, + } +} diff --git a/packages/react-query/src/utils-router.ts b/packages/react-query/src/utils-router.ts new file mode 100644 index 00000000..b9172059 --- /dev/null +++ b/packages/react-query/src/utils-router.ts @@ -0,0 +1,59 @@ +import type { ContractProcedure, ContractRouter, SchemaInput, SchemaOutput } from '@orpc/contract' +import type { Lazy, Procedure, Router } from '@orpc/server' +import { createGeneralUtils, type GeneralUtils } from './utils-general' +import { createProcedureUtils, type ProcedureUtils } from './utils-procedure' + +export type RouterUtils | ContractRouter> = { + [K in keyof T]: T[K] extends + | ContractProcedure + | Procedure + | Lazy> + ? + & ProcedureUtils, SchemaOutput> + & GeneralUtils> + : T[K] extends Router | ContractRouter + ? RouterUtils + : never +} + +export interface CreateRouterUtilsOptions<_T extends Router | ContractRouter> { + prefix: string + client: any // TODO +} + +export function createRouterUtils | ContractRouter>( + options: CreateRouterUtilsOptions, +): RouterUtils { + const generalUtils = createGeneralUtils(options.prefix, []) // TODO + const procedureUtils = createProcedureUtils(options.prefix, options.client) + + const recursive = new Proxy({ + ...generalUtils, + ...procedureUtils, + }, { + get(target, prop) { + const value = Reflect.get(target, prop) + + if (typeof prop !== 'string') { + return value + } + + const nextUtils = createRouterUtils({ + prefix: options.prefix, + client: options.client[prop], + }) + + if (typeof value !== 'function') { + return nextUtils + } + + return new Proxy(value, { + get(_, prop) { + return Reflect.get(nextUtils, prop) + }, + }) + }, + }) + + return recursive as any +} diff --git a/packages/react-query/tsconfig.json b/packages/react-query/tsconfig.json index b64920aa..49afa752 100644 --- a/packages/react-query/tsconfig.json +++ b/packages/react-query/tsconfig.json @@ -3,7 +3,12 @@ "compilerOptions": { "types": [] }, - "references": [], + "references": [ + { "path": "../client" }, + { "path": "../contract" }, + { "path": "../shared" }, + { "path": "../server" } + ], "include": ["src"], "exclude": [ "**/*.test.*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f1615336..55749386 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -260,7 +260,26 @@ importers: specifier: ^3.23.8 version: 3.23.8 - packages/react-query: {} + packages/react-query: + dependencies: + '@orpc/client': + specifier: workspace:* + version: link:../client + '@orpc/contract': + specifier: workspace:* + version: link:../contract + '@orpc/server': + specifier: workspace:* + version: link:../server + '@orpc/shared': + specifier: workspace:* + version: link:../shared + '@tanstack/react-query': + specifier: '>=5.59.0' + version: 5.59.15(react@18.3.1) + react: + specifier: '>=18.3.0' + version: 18.3.1 packages/server: dependencies: From 940406dfbfad947649270e2362ff546b1a9eb2a1 Mon Sep 17 00:00:00 2001 From: unnoq Date: Mon, 9 Dec 2024 15:26:32 +0700 Subject: [PATCH 03/17] tests --- packages/react-query/package.json | 3 + packages/react-query/src/key.test.ts | 17 ++ packages/react-query/src/key.ts | 7 +- .../react-query/src/utils-general.test-d.ts | 30 +++ .../react-query/src/utils-general.test.ts | 13 ++ packages/react-query/src/utils-general.ts | 5 +- .../react-query/src/utils-procedure.test-d.ts | 173 ++++++++++++++++++ packages/react-query/src/utils-procedure.ts | 13 +- .../react-query/src/utils-router.test-d.ts | 57 ++++++ packages/react-query/src/utils-router.ts | 2 +- pnpm-lock.yaml | 4 + 11 files changed, 312 insertions(+), 12 deletions(-) create mode 100644 packages/react-query/src/key.test.ts create mode 100644 packages/react-query/src/utils-general.test-d.ts create mode 100644 packages/react-query/src/utils-general.test.ts create mode 100644 packages/react-query/src/utils-procedure.test-d.ts create mode 100644 packages/react-query/src/utils-router.test-d.ts diff --git a/packages/react-query/package.json b/packages/react-query/package.json index 7527c39e..e3e066ac 100644 --- a/packages/react-query/package.json +++ b/packages/react-query/package.json @@ -53,5 +53,8 @@ }, "dependencies": { "@orpc/shared": "workspace:*" + }, + "devDependencies": { + "zod": "^3.21.4" } } diff --git a/packages/react-query/src/key.test.ts b/packages/react-query/src/key.test.ts new file mode 100644 index 00000000..a9984028 --- /dev/null +++ b/packages/react-query/src/key.test.ts @@ -0,0 +1,17 @@ +import { buildKey } from './key' + +describe('buildKey', () => { + it('works', () => { + expect(buildKey('__ORPC__', ['path'])).toEqual(['__ORPC__', ['path'], {}]) + expect(buildKey('__ORPC__', ['path'], { type: 'query' })).toEqual(['__ORPC__', ['path'], { type: 'query' }]) + + expect(buildKey('__ORPC__', ['path'], { type: 'query', input: { a: 1 } })) + .toEqual(['__ORPC__', ['path'], { type: 'query', input: { a: 1 } }]) + + expect(buildKey('__ORPC__', ['path'], { type: 'query', input: undefined })) + .toEqual(['__ORPC__', ['path'], { type: 'query' }]) + + expect(buildKey('__ORPC__', ['path'], { type: undefined, input: undefined })) + .toEqual(['__ORPC__', ['path'], { }]) + }) +}) diff --git a/packages/react-query/src/key.ts b/packages/react-query/src/key.ts index 951d4456..6a54a016 100644 --- a/packages/react-query/src/key.ts +++ b/packages/react-query/src/key.ts @@ -1,13 +1,14 @@ +import type { PartialDeep } from '@orpc/shared' import type { QueryKey } from '@tanstack/react-query' export type KeyType = 'query' | 'infinite' | 'mutation' | undefined -export interface BuildKeyOptions { +export interface BuildKeyOptions { type?: TType - input?: TType extends 'mutation' ? never : TInput + input?: TType extends 'mutation' ? never : PartialDeep } -export function buildKey( +export function buildKey( prefix: string, path: string[], options?: BuildKeyOptions, diff --git a/packages/react-query/src/utils-general.test-d.ts b/packages/react-query/src/utils-general.test-d.ts new file mode 100644 index 00000000..ba5f6c95 --- /dev/null +++ b/packages/react-query/src/utils-general.test-d.ts @@ -0,0 +1,30 @@ +import { createGeneralUtils } from './utils-general' + +describe('key', () => { + const utils = createGeneralUtils<{ a: { b: { c: number } } }>('__ORPC__', []) + + it('infer correct input type & partial input', () => { + utils.key() + utils.key({}) + utils.key({ type: 'infinite' }) + utils.key({ input: {}, type: 'query' }) + utils.key({ input: {} }) + utils.key({ input: { a: {} } }) + utils.key({ input: { a: { b: {} } } }) + utils.key({ input: { a: { b: { c: 1 } } } }) + + // @ts-expect-error invalid input + utils.key({ input: 123 }) + // @ts-expect-error invalid input + utils.key({ input: { a: { b: { c: '1' } } } }) + + // @ts-expect-error invalid input + utils.key({ type: 'ddd' }) + }) + + it('it prevent pass input when type is mutation', () => { + utils.key({ type: 'mutation' }) + // @ts-expect-error input is not allowed when type is mutation + utils.key({ input: {}, type: 'mutation' }) + }) +}) diff --git a/packages/react-query/src/utils-general.test.ts b/packages/react-query/src/utils-general.test.ts new file mode 100644 index 00000000..489650a5 --- /dev/null +++ b/packages/react-query/src/utils-general.test.ts @@ -0,0 +1,13 @@ +import * as keyModule from './key' +import { createGeneralUtils } from './utils-general' + +const buildKeySpy = vi.spyOn(keyModule, 'buildKey') + +describe('key', () => { + it('works', () => { + const utils = createGeneralUtils('__ORPC__', ['path']) + expect(utils.key({ input: 'input', type: 'infinite' })).toEqual(['__ORPC__', ['path'], { input: 'input', type: 'infinite' }]) + expect(buildKeySpy).toHaveBeenCalledTimes(1) + expect(buildKeySpy).toHaveBeenCalledWith('__ORPC__', ['path'], { input: 'input', type: 'infinite' }) + }) +}) diff --git a/packages/react-query/src/utils-general.ts b/packages/react-query/src/utils-general.ts index f711642b..dc4ef7a7 100644 --- a/packages/react-query/src/utils-general.ts +++ b/packages/react-query/src/utils-general.ts @@ -1,11 +1,12 @@ import type { QueryKey } from '@tanstack/react-query' -import { buildKey, type BuildKeyOptions } from './key' +import type { BuildKeyOptions, KeyType } from './key' +import { buildKey } from './key' /** * Utils at any level (procedure or router) */ export interface GeneralUtils { - key: (options?: BuildKeyOptions) => QueryKey + key: (options?: BuildKeyOptions) => QueryKey } export function createGeneralUtils( diff --git a/packages/react-query/src/utils-procedure.test-d.ts b/packages/react-query/src/utils-procedure.test-d.ts new file mode 100644 index 00000000..c654cb93 --- /dev/null +++ b/packages/react-query/src/utils-procedure.test-d.ts @@ -0,0 +1,173 @@ +import type { InfiniteData } from '@tanstack/react-query' +import { useInfiniteQuery, useQuery } from '@tanstack/react-query' +import { createProcedureUtils } from './utils-procedure' + +describe('queryOptions', () => { + const client = vi.fn((input: number | undefined) => Promise.resolve(input?.toString())) + const utils = createProcedureUtils('__ORPC__', client) + + const client2 = vi.fn((input: number) => Promise.resolve(input.toString())) + const utils2 = createProcedureUtils('__ORPC__', client2) + + it('infer correct input type', () => { + utils.queryOptions({ input: 1 }) + utils.queryOptions({ input: undefined }) + + // @ts-expect-error invalid input + utils.queryOptions({ input: '1' }) + }) + + it('can be called without argument', () => { + utils.queryOptions() + // @ts-expect-error invalid is required + utils2.queryOptions() + }) + + it('infer correct output type', () => { + const query = useQuery(utils2.queryOptions({ input: 1 })) + + if (query.status === 'success') { + expectTypeOf(query.data).toEqualTypeOf() + } + }) + + it('work with select options', () => { + const query = useQuery(utils2.queryOptions({ + input: 1, + select(data) { + expectTypeOf(data).toEqualTypeOf() + + return 'new' as const + }, + })) + + if (query.status === 'success') { + expectTypeOf(query.data).toEqualTypeOf<'new'>() + } + }) +}) + +describe('infiniteOptions', () => { + const getNextPageParam = vi.fn() + + it('cannot use on procedure without input object-able', () => { + const utils = createProcedureUtils('__ORPC__', {} as (input: number) => Promise) + + // @ts-expect-error missing initialPageParam + utils.infiniteOptions({ + input: 123, + getNextPageParam, + }) + + utils.infiniteOptions({ + input: 123, + getNextPageParam, + // @ts-expect-error initialPageParam must be never + initialPageParam: 123, + }) + + utils.infiniteOptions({ + input: 123, + getNextPageParam, + initialPageParam: 123 as never, + }) + }) + + it('infer correct input type', () => { + const utils = createProcedureUtils('__ORPC__', {} as (input: { limit?: number, cursor: number }) => Promise) + + utils.infiniteOptions({ + input: { + limit: 1, + }, + getNextPageParam, + initialPageParam: 1, + }) + utils.infiniteOptions({ + input: { + limit: undefined, + }, + getNextPageParam, + initialPageParam: 1, + + }) + + utils.infiniteOptions({ + input: { + // @ts-expect-error invalid input + limit: 'string', + // cursor will be ignored + cursor: {}, + }, + getNextPageParam, + initialPageParam: 1, + }) + }) + + it('infer correct initialPageParam type', () => { + const utils = createProcedureUtils('__ORPC__', {} as (input: { limit?: number, cursor: number }) => Promise) + + utils.infiniteOptions({ + input: {}, + getNextPageParam, + initialPageParam: 1, + }) + + utils.infiniteOptions({ + input: {}, + getNextPageParam, + // @ts-expect-error initialPageParam must be number + initialPageParam: '234', + }) + + // @ts-expect-error initialPageParam is required + utils.infiniteOptions({ + input: {}, + getNextPageParam, + }) + }) + + it('initialPageParam can be optional', () => { + const utils = createProcedureUtils('__ORPC__', {} as (input: { limit?: number, cursor?: number }) => Promise) + + utils.infiniteOptions({ + input: {}, + getNextPageParam, + }) + }) + + it('infer correct output type', () => { + const utils = createProcedureUtils('__ORPC__', {} as (input: { limit?: number, cursor: number }) => Promise) + const query = useInfiniteQuery(utils.infiniteOptions({ + input: { + limit: 1, + }, + getNextPageParam, + initialPageParam: 1, + })) + + if (query.status === 'success') { + expectTypeOf(query.data).toEqualTypeOf>() + } + }) + + it('work with select options', () => { + const utils = createProcedureUtils('__ORPC__', {} as (input: { limit?: number, cursor: number }) => Promise) + const query = useInfiniteQuery(utils.infiniteOptions({ + input: { + limit: 1, + }, + getNextPageParam, + initialPageParam: 1, + select(data) { + expectTypeOf(data).toEqualTypeOf>() + + return 'new' as const + }, + })) + + if (query.status === 'success') { + expectTypeOf(query.data).toEqualTypeOf<'new'>() + } + }) +}) diff --git a/packages/react-query/src/utils-procedure.ts b/packages/react-query/src/utils-procedure.ts index 21ad3fb9..0f95d2b6 100644 --- a/packages/react-query/src/utils-procedure.ts +++ b/packages/react-query/src/utils-procedure.ts @@ -8,9 +8,10 @@ import { buildKey } from './key' */ export interface ProcedureUtils { queryOptions: ( - options: + ...options: [ & SetOptional, 'queryFn' | 'queryKey'> - & PartialOnUndefinedDeep<{ input: TInput }> + & PartialOnUndefinedDeep<{ input: TInput }>, + ] | (undefined extends TInput ? [] : never) ) => UseQueryOptions infiniteOptions: >>( @@ -36,18 +37,18 @@ export function createProcedureUtils( const path: string[] = [] // TODO return { - queryOptions(options) { - const input = options.input as TInput + queryOptions(...[options]) { + const input = options?.input return { queryKey: buildKey(prefix, path, { type: 'query', input }), - queryFn: () => client(input), + queryFn: () => client(input as TInput), ...options, } }, infiniteOptions(options) { - const input = options.input as Omit + const input = options.input return { queryKey: buildKey(prefix, path, { type: 'infinite', input }), diff --git a/packages/react-query/src/utils-router.test-d.ts b/packages/react-query/src/utils-router.test-d.ts new file mode 100644 index 00000000..779c2a54 --- /dev/null +++ b/packages/react-query/src/utils-router.test-d.ts @@ -0,0 +1,57 @@ +import { oc } from '@orpc/contract' +import { os } from '@orpc/server' +import { z } from 'zod' +import { createGeneralUtils } from './utils-general' +import { createProcedureUtils } from './utils-procedure' +import { createRouterUtils } from './utils-router' + +const pingContract = oc.input(z.object({ name: z.string() })).output(z.string()) +const pongContract = oc.input(z.number()).output(z.string()) +const contractRouter = oc.router({ + ping: pingContract, + pong: pongContract, +}) + +const ping = os.contract(pingContract).func(({ name }) => `ping ${name}`) +const pong = os.contract(pongContract).func(num => `pong ${num}`) + +const router = os.contract(contractRouter).router({ + ping, + pong: os.lazy(() => Promise.resolve({ default: pong })), +}) + +describe('with contract router', () => { + it('build correct types', () => { + const utils = createRouterUtils({} as any) + + const generalUtils = createGeneralUtils('__ORPC__', []) + const pingUtils = createProcedureUtils('__ORPC__', ping) + const pingGeneralUtils = createGeneralUtils<{ name: string }>('__ORPC__', ['ping']) + const pongUtils = createProcedureUtils('__ORPC__', pong) + const pongGeneralUtils = createGeneralUtils('__ORPC__', ['ping']) + + expectTypeOf(utils).toMatchTypeOf() + expectTypeOf(utils.ping).toMatchTypeOf() + expectTypeOf(utils.ping).toMatchTypeOf() + expectTypeOf(utils.pong).toMatchTypeOf() + expectTypeOf(utils.pong).toMatchTypeOf() + }) +}) + +describe('with router', () => { + it('build correct types', () => { + const utils = createRouterUtils({} as any) + + const generalUtils = createGeneralUtils('__ORPC__', []) + const pingUtils = createProcedureUtils('__ORPC__', ping) + const pingGeneralUtils = createGeneralUtils<{ name: string }>('__ORPC__', ['ping']) + const pongUtils = createProcedureUtils('__ORPC__', pong) + const pongGeneralUtils = createGeneralUtils('__ORPC__', ['ping']) + + 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 b9172059..bd239906 100644 --- a/packages/react-query/src/utils-router.ts +++ b/packages/react-query/src/utils-router.ts @@ -14,7 +14,7 @@ export type RouterUtils | ContractRouter> = { : T[K] extends Router | ContractRouter ? RouterUtils : never -} +} & GeneralUtils export interface CreateRouterUtilsOptions<_T extends Router | ContractRouter> { prefix: string diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55749386..d3929420 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -280,6 +280,10 @@ importers: react: specifier: '>=18.3.0' version: 18.3.1 + devDependencies: + zod: + specifier: ^3.21.4 + version: 3.23.8 packages/server: dependencies: From a8d103cd636b74c49ca998bef07ef4abf5aaee41 Mon Sep 17 00:00:00 2001 From: unnoq Date: Mon, 9 Dec 2024 19:01:55 +0700 Subject: [PATCH 04/17] path --- .../react-query/src/utils-procedure.test-d.ts | 16 +++++++------- packages/react-query/src/utils-procedure.ts | 5 ++--- .../react-query/src/utils-router.test-d.ts | 8 +++---- packages/react-query/src/utils-router.ts | 22 ++++++++++--------- 4 files changed, 26 insertions(+), 25 deletions(-) diff --git a/packages/react-query/src/utils-procedure.test-d.ts b/packages/react-query/src/utils-procedure.test-d.ts index c654cb93..c0a440af 100644 --- a/packages/react-query/src/utils-procedure.test-d.ts +++ b/packages/react-query/src/utils-procedure.test-d.ts @@ -4,10 +4,10 @@ import { createProcedureUtils } from './utils-procedure' describe('queryOptions', () => { const client = vi.fn((input: number | undefined) => Promise.resolve(input?.toString())) - const utils = createProcedureUtils('__ORPC__', client) + const utils = createProcedureUtils(client, '__ORPC__', []) const client2 = vi.fn((input: number) => Promise.resolve(input.toString())) - const utils2 = createProcedureUtils('__ORPC__', client2) + const utils2 = createProcedureUtils(client2, '__ORPC__', []) it('infer correct input type', () => { utils.queryOptions({ input: 1 }) @@ -51,7 +51,7 @@ describe('infiniteOptions', () => { const getNextPageParam = vi.fn() it('cannot use on procedure without input object-able', () => { - const utils = createProcedureUtils('__ORPC__', {} as (input: number) => Promise) + const utils = createProcedureUtils({} as (input: number) => Promise, '__ORPC__', []) // @ts-expect-error missing initialPageParam utils.infiniteOptions({ @@ -74,7 +74,7 @@ describe('infiniteOptions', () => { }) it('infer correct input type', () => { - const utils = createProcedureUtils('__ORPC__', {} as (input: { limit?: number, cursor: number }) => Promise) + const utils = createProcedureUtils({} as (input: { limit?: number, cursor: number }) => Promise, '__ORPC__', []) utils.infiniteOptions({ input: { @@ -105,7 +105,7 @@ describe('infiniteOptions', () => { }) it('infer correct initialPageParam type', () => { - const utils = createProcedureUtils('__ORPC__', {} as (input: { limit?: number, cursor: number }) => Promise) + const utils = createProcedureUtils({} as (input: { limit?: number, cursor: number }) => Promise, '__ORPC__', []) utils.infiniteOptions({ input: {}, @@ -128,7 +128,7 @@ describe('infiniteOptions', () => { }) it('initialPageParam can be optional', () => { - const utils = createProcedureUtils('__ORPC__', {} as (input: { limit?: number, cursor?: number }) => Promise) + const utils = createProcedureUtils({} as (input: { limit?: number, cursor?: number }) => Promise, '__ORPC__', []) utils.infiniteOptions({ input: {}, @@ -137,7 +137,7 @@ describe('infiniteOptions', () => { }) it('infer correct output type', () => { - const utils = createProcedureUtils('__ORPC__', {} as (input: { limit?: number, cursor: number }) => Promise) + const utils = createProcedureUtils({} as (input: { limit?: number, cursor: number }) => Promise, '__ORPC__', []) const query = useInfiniteQuery(utils.infiniteOptions({ input: { limit: 1, @@ -152,7 +152,7 @@ describe('infiniteOptions', () => { }) it('work with select options', () => { - const utils = createProcedureUtils('__ORPC__', {} as (input: { limit?: number, cursor: number }) => Promise) + const utils = createProcedureUtils({} as (input: { limit?: number, cursor: number }) => Promise, '__ORPC__', []) const query = useInfiniteQuery(utils.infiniteOptions({ input: { limit: 1, diff --git a/packages/react-query/src/utils-procedure.ts b/packages/react-query/src/utils-procedure.ts index 0f95d2b6..af360bf3 100644 --- a/packages/react-query/src/utils-procedure.ts +++ b/packages/react-query/src/utils-procedure.ts @@ -31,11 +31,10 @@ export interface ProcedureUtils { } export function createProcedureUtils( - prefix: string, client: (input: TInput) => Promise, + prefix: string, + path: string[], ): ProcedureUtils { - const path: string[] = [] // TODO - return { queryOptions(...[options]) { const input = options?.input diff --git a/packages/react-query/src/utils-router.test-d.ts b/packages/react-query/src/utils-router.test-d.ts index 779c2a54..1918b184 100644 --- a/packages/react-query/src/utils-router.test-d.ts +++ b/packages/react-query/src/utils-router.test-d.ts @@ -25,9 +25,9 @@ describe('with contract router', () => { const utils = createRouterUtils({} as any) const generalUtils = createGeneralUtils('__ORPC__', []) - const pingUtils = createProcedureUtils('__ORPC__', ping) + const pingUtils = createProcedureUtils(ping, '__ORPC__', []) const pingGeneralUtils = createGeneralUtils<{ name: string }>('__ORPC__', ['ping']) - const pongUtils = createProcedureUtils('__ORPC__', pong) + const pongUtils = createProcedureUtils(pong, '__ORPC__', []) const pongGeneralUtils = createGeneralUtils('__ORPC__', ['ping']) expectTypeOf(utils).toMatchTypeOf() @@ -43,9 +43,9 @@ describe('with router', () => { const utils = createRouterUtils({} as any) const generalUtils = createGeneralUtils('__ORPC__', []) - const pingUtils = createProcedureUtils('__ORPC__', ping) + const pingUtils = createProcedureUtils(ping, '__ORPC__', []) const pingGeneralUtils = createGeneralUtils<{ name: string }>('__ORPC__', ['ping']) - const pongUtils = createProcedureUtils('__ORPC__', pong) + const pongUtils = createProcedureUtils(pong, '__ORPC__', []) const pongGeneralUtils = createGeneralUtils('__ORPC__', ['ping']) expectTypeOf(utils).toMatchTypeOf() diff --git a/packages/react-query/src/utils-router.ts b/packages/react-query/src/utils-router.ts index bd239906..cc014a02 100644 --- a/packages/react-query/src/utils-router.ts +++ b/packages/react-query/src/utils-router.ts @@ -16,16 +16,18 @@ export type RouterUtils | ContractRouter> = { : never } & GeneralUtils -export interface CreateRouterUtilsOptions<_T extends Router | ContractRouter> { - prefix: string - client: any // TODO -} - +/** + * @param client - The client create form `@orpc/client` + * @param prefix - Prefix query, mutation key help you prevent conflict with other query or mutation + * @param path - The path of the procedure + */ export function createRouterUtils | ContractRouter>( - options: CreateRouterUtilsOptions, + client: any, // TODO typed + prefix: string = '__oRPC__', + path: string[] = [], ): RouterUtils { - const generalUtils = createGeneralUtils(options.prefix, []) // TODO - const procedureUtils = createProcedureUtils(options.prefix, options.client) + const generalUtils = createGeneralUtils(prefix, path) + const procedureUtils = createProcedureUtils(client, prefix, path) const recursive = new Proxy({ ...generalUtils, @@ -39,8 +41,8 @@ export function createRouterUtils | ContractRouter>( } const nextUtils = createRouterUtils({ - prefix: options.prefix, - client: options.client[prop], + prefix, + client: client[prop], }) if (typeof value !== 'function') { From 5734df006adba97a4734427d6e24dee412560cd8 Mon Sep 17 00:00:00 2001 From: unnoq Date: Mon, 9 Dec 2024 19:33:08 +0700 Subject: [PATCH 05/17] tests --- .../react-query/src/utils-procedure.test.ts | 66 +++++++++++++++++++ packages/react-query/src/utils-router.test.ts | 56 ++++++++++++++++ packages/react-query/src/utils-router.ts | 5 +- 3 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 packages/react-query/src/utils-procedure.test.ts create mode 100644 packages/react-query/src/utils-router.test.ts diff --git a/packages/react-query/src/utils-procedure.test.ts b/packages/react-query/src/utils-procedure.test.ts new file mode 100644 index 00000000..819cbb25 --- /dev/null +++ b/packages/react-query/src/utils-procedure.test.ts @@ -0,0 +1,66 @@ +import * as keyModule from './key' +import { createProcedureUtils } from './utils-procedure' + +const buildKeySpy = vi.spyOn(keyModule, 'buildKey') + +describe('queryOptions', () => { + const client = vi.fn((input: number | undefined) => Promise.resolve(input?.toString())) + const utils = createProcedureUtils(client, '__ORPC__', ['ping']) + + it('works', async () => { + const options = utils.queryOptions({ input: 1 }) + + expect(options.queryKey).toEqual(['__ORPC__', ['ping'], { type: 'query', input: 1 }]) + expect(buildKeySpy).toHaveBeenCalledTimes(1) + expect(buildKeySpy).toHaveBeenCalledWith('__ORPC__', ['ping'], { type: 'query', input: 1 }) + + client.mockResolvedValueOnce('__mocked__') + await expect((options as any).queryFn()).resolves.toEqual('__mocked__') + expect(client).toHaveBeenCalledTimes(1) + expect(client).toBeCalledWith(1) + }) +}) + +describe('infiniteOptions', () => { + const getNextPageParam = vi.fn() + + it('works ', async () => { + const client = vi.fn<(input: { limit?: number, cursor: number | undefined }) => Promise>() + const utils = createProcedureUtils(client, '__ORPC__', []) + + const options = utils.infiniteOptions({ + input: { limit: 5 }, + getNextPageParam, + initialPageParam: 1, + }) + + expect(options.initialPageParam).toEqual(1) + expect(options.queryKey).toEqual(['__ORPC__', [], { type: 'infinite', input: { limit: 5 } }]) + expect(buildKeySpy).toHaveBeenCalledTimes(1) + expect(buildKeySpy).toHaveBeenCalledWith('__ORPC__', [], { type: 'infinite', input: { limit: 5 } }) + + client.mockResolvedValueOnce('__mocked__') + await expect((options as any).queryFn({ pageParam: 1 })).resolves.toEqual('__mocked__') + expect(client).toHaveBeenCalledTimes(1) + expect(client).toBeCalledWith({ limit: 5, cursor: 1 }) + }) + + it('works without initialPageParam', async () => { + const client = vi.fn<(input: { limit?: number, cursor: number | undefined }) => Promise>() + const utils = createProcedureUtils(client, '__ORPC__', []) + + const options = utils.infiniteOptions({ + input: { limit: 5 }, + getNextPageParam, + }) + + expect(options.queryKey).toEqual(['__ORPC__', [], { type: 'infinite', input: { limit: 5 } }]) + expect(buildKeySpy).toHaveBeenCalledTimes(1) + expect(buildKeySpy).toHaveBeenCalledWith('__ORPC__', [], { type: 'infinite', input: { limit: 5 } }) + + client.mockResolvedValueOnce('__mocked__') + await expect((options as any).queryFn({ pageParam: undefined })).resolves.toEqual('__mocked__') + expect(client).toHaveBeenCalledTimes(1) + expect(client).toBeCalledWith({ limit: 5, cursor: undefined }) + }) +}) diff --git a/packages/react-query/src/utils-router.test.ts b/packages/react-query/src/utils-router.test.ts new file mode 100644 index 00000000..cacc5e91 --- /dev/null +++ b/packages/react-query/src/utils-router.test.ts @@ -0,0 +1,56 @@ +import * as generalUtilsModule from './utils-general' +import * as procedureUtilsModule from './utils-procedure' +import { createRouterUtils } from './utils-router' + +const procedureUtilsSpy = vi.spyOn(procedureUtilsModule, 'createProcedureUtils') +const generalUtilsSpy = vi.spyOn(generalUtilsModule, 'createGeneralUtils') + +beforeEach(() => { + procedureUtilsSpy.mockClear() + generalUtilsSpy.mockClear() +}) + +describe('router utils', () => { + it('works', () => { + const client = vi.fn() as any + client.ping = vi.fn() + client.ping.peng = vi.fn() + + const utils = createRouterUtils(client) as any + + expect(generalUtilsSpy).toHaveBeenCalledTimes(1) + expect(generalUtilsSpy).toHaveBeenCalledWith('__oRPC__', []) + expect(procedureUtilsSpy).toHaveBeenCalledTimes(1) + expect(procedureUtilsSpy).toHaveBeenCalledWith(client, '__oRPC__', []) + + expect(typeof utils.key).toEqual('function') + expect(typeof utils.queryOptions).toEqual('function') + + generalUtilsSpy.mockClear() + procedureUtilsSpy.mockClear() + void utils.ping + + expect(generalUtilsSpy).toHaveBeenCalledTimes(1) + expect(generalUtilsSpy).toHaveBeenCalledWith('__oRPC__', ['ping']) + expect(procedureUtilsSpy).toHaveBeenCalledTimes(1) + expect(procedureUtilsSpy).toHaveBeenCalledWith(client.ping, '__oRPC__', ['ping']) + + expect(typeof utils.ping.key).toEqual('function') + expect(typeof utils.ping.queryOptions).toEqual('function') + + generalUtilsSpy.mockClear() + procedureUtilsSpy.mockClear() + void utils.ping.peng + + expect(generalUtilsSpy).toHaveBeenCalledTimes(2) + expect(generalUtilsSpy).toHaveBeenNthCalledWith(1, '__oRPC__', ['ping']) + expect(generalUtilsSpy).toHaveBeenNthCalledWith(2, '__oRPC__', ['ping', 'peng']) + + expect(procedureUtilsSpy).toHaveBeenCalledTimes(2) + expect(procedureUtilsSpy).toHaveBeenNthCalledWith(1, client.ping, '__oRPC__', ['ping']) + expect(procedureUtilsSpy).toHaveBeenNthCalledWith(2, client.ping.peng, '__oRPC__', ['ping', 'peng']) + + expect(typeof utils.ping.peng.key).toEqual('function') + expect(typeof utils.ping.peng.queryOptions).toEqual('function') + }) +}) diff --git a/packages/react-query/src/utils-router.ts b/packages/react-query/src/utils-router.ts index cc014a02..3ec6ab43 100644 --- a/packages/react-query/src/utils-router.ts +++ b/packages/react-query/src/utils-router.ts @@ -40,10 +40,7 @@ export function createRouterUtils | ContractRouter>( return value } - const nextUtils = createRouterUtils({ - prefix, - client: client[prop], - }) + const nextUtils = createRouterUtils(client[prop], prefix, [...path, prop]) if (typeof value !== 'function') { return nextUtils From e2a3abf07c659eb2016b1092e38232c03fe7e560 Mon Sep 17 00:00:00 2001 From: unnoq Date: Mon, 9 Dec 2024 20:08:37 +0700 Subject: [PATCH 06/17] tests --- .../react-query/src/utils-general.test.ts | 4 + .../react-query/src/utils-procedure.test-d.ts | 37 ++++++ .../react-query/src/utils-procedure.test.ts | 4 + packages/react-query/src/utils-procedure.ts | 2 +- packages/react-query/src/utils-router.test.ts | 11 ++ packages/react-query/tests/e2e.test.tsx | 89 +++++++++++++ packages/react-query/tests/helpers.tsx | 117 ++++++++++++++++++ vitest.workspace.ts | 2 +- 8 files changed, 264 insertions(+), 2 deletions(-) create mode 100644 packages/react-query/tests/e2e.test.tsx create mode 100644 packages/react-query/tests/helpers.tsx diff --git a/packages/react-query/src/utils-general.test.ts b/packages/react-query/src/utils-general.test.ts index 489650a5..d14ce098 100644 --- a/packages/react-query/src/utils-general.test.ts +++ b/packages/react-query/src/utils-general.test.ts @@ -3,6 +3,10 @@ import { createGeneralUtils } from './utils-general' const buildKeySpy = vi.spyOn(keyModule, 'buildKey') +beforeEach(() => { + buildKeySpy.mockClear() +}) + describe('key', () => { it('works', () => { const utils = createGeneralUtils('__ORPC__', ['path']) diff --git a/packages/react-query/src/utils-procedure.test-d.ts b/packages/react-query/src/utils-procedure.test-d.ts index c0a440af..768d2472 100644 --- a/packages/react-query/src/utils-procedure.test-d.ts +++ b/packages/react-query/src/utils-procedure.test-d.ts @@ -171,3 +171,40 @@ describe('infiniteOptions', () => { } }) }) + +describe('mutationOptions', () => { + const client = vi.fn((input: number) => Promise.resolve(input.toString())) + const utils = createProcedureUtils(client, '__ORPC__', []) + + it('infer correct input type', () => { + const option = utils.mutationOptions({ + onSuccess: (data, input) => { + expectTypeOf(input).toEqualTypeOf() + }, + }) + + option.mutationFn!(1) + + // @ts-expect-error invalid input + option.mutationFn!('1') + // @ts-expect-error invalid input + option.mutationFn!() + }) + + it('infer correct output type', () => { + const option = utils.mutationOptions({ + onSuccess: (data, input) => { + expectTypeOf(input).toEqualTypeOf() + expectTypeOf(data).toEqualTypeOf() + }, + }) + + expectTypeOf(option.mutationFn!(1)).toEqualTypeOf >() + }) + + it('can be called without argument', () => { + const option = utils.mutationOptions() + + expectTypeOf(option.mutationFn!(1)).toEqualTypeOf >() + }) +}) diff --git a/packages/react-query/src/utils-procedure.test.ts b/packages/react-query/src/utils-procedure.test.ts index 819cbb25..0520847d 100644 --- a/packages/react-query/src/utils-procedure.test.ts +++ b/packages/react-query/src/utils-procedure.test.ts @@ -3,6 +3,10 @@ import { createProcedureUtils } from './utils-procedure' const buildKeySpy = vi.spyOn(keyModule, 'buildKey') +beforeEach(() => { + buildKeySpy.mockClear() +}) + describe('queryOptions', () => { const client = vi.fn((input: number | undefined) => Promise.resolve(input?.toString())) const utils = createProcedureUtils(client, '__ORPC__', ['ping']) diff --git a/packages/react-query/src/utils-procedure.ts b/packages/react-query/src/utils-procedure.ts index af360bf3..1c39c3b6 100644 --- a/packages/react-query/src/utils-procedure.ts +++ b/packages/react-query/src/utils-procedure.ts @@ -26,7 +26,7 @@ export interface ProcedureUtils { ) => UseInfiniteQueryOptions> mutationOptions: ( - options: SetOptional, 'mutationFn' | 'mutationKey'> + options?: SetOptional, 'mutationFn' | 'mutationKey'> ) => UseMutationOptions } diff --git a/packages/react-query/src/utils-router.test.ts b/packages/react-query/src/utils-router.test.ts index cacc5e91..5623aba2 100644 --- a/packages/react-query/src/utils-router.test.ts +++ b/packages/react-query/src/utils-router.test.ts @@ -53,4 +53,15 @@ describe('router utils', () => { expect(typeof utils.ping.peng.key).toEqual('function') expect(typeof utils.ping.peng.queryOptions).toEqual('function') }) + + it('can custom prefix and base path', () => { + const client = vi.fn() as any + + const utils = createRouterUtils(client, 'prefix', ['base']) as any + + expect(generalUtilsSpy).toHaveBeenCalledTimes(1) + expect(generalUtilsSpy).toHaveBeenCalledWith('prefix', ['base']) + expect(procedureUtilsSpy).toHaveBeenCalledTimes(1) + expect(procedureUtilsSpy).toHaveBeenCalledWith(client, 'prefix', ['base']) + }) }) diff --git a/packages/react-query/tests/e2e.test.tsx b/packages/react-query/tests/e2e.test.tsx new file mode 100644 index 00000000..f7de841b --- /dev/null +++ b/packages/react-query/tests/e2e.test.tsx @@ -0,0 +1,89 @@ +import { useInfiniteQuery, useMutation, useQuery, useSuspenseInfiniteQuery, useSuspenseQuery } from '@tanstack/react-query' +import { renderHook } from '@testing-library/react' +import { orpc, queryClient } from './helpers' + +beforeEach(() => { + queryClient.clear() +}) + +describe('useQuery', () => { + it('works - onSuccess', async () => { + const { result } = renderHook(() => useQuery(orpc.ping.queryOptions(), queryClient)) + + expect(queryClient.isFetching({ queryKey: orpc.key() })).toEqual(1) + expect(queryClient.isFetching({ queryKey: orpc.ping.key() })).toEqual(1) + expect(queryClient.isFetching({ queryKey: orpc.ping.key({ type: 'mutation' }) })).toEqual(0) + + await vi.waitFor(() => expect(result.current.data).toEqual('pong')) + + expect(queryClient.getQueryData(orpc.ping.key({ type: 'query' }))).toEqual('pong') + }) + + it('works - onError', async () => { + // @ts-expect-error -- invalid input + const { result } = renderHook(() => useQuery(orpc.user.create.queryOptions({ input: {} }), queryClient)) + + await vi.waitFor(() => expect(result.current.error).toEqual(new Error('Validation input failed'))) + + expect(queryClient.getQueryData(orpc.ping.key({ type: 'query' }))).toEqual(undefined) + }) +}) + +describe('useInfiniteQuery', () => { + it('works - onSuccess', async () => { + const { result } = renderHook(() => useInfiniteQuery(orpc.user.list.infiniteOptions({ + input: {}, + getNextPageParam: lastPage => lastPage.nextCursor, + }), queryClient)) + + await vi.waitFor(() => expect(result.current.data?.pages.length).toEqual(1)) + + result.current.fetchNextPage() + + await vi.waitFor(() => expect(result.current.data?.pages.length).toEqual(2)) + }) +}) + +describe('useMutation', () => { + it('works - onSuccess', async () => { + const { result } = renderHook(() => useMutation(orpc.ping.mutationOptions(), queryClient)) + + expect(queryClient.isFetching({ queryKey: orpc.ping.key({ type: 'mutation' }) })).toEqual(0) + + result.current.mutate({}) + + expect(queryClient.isMutating({ mutationKey: orpc.ping.key() })).toEqual(1) + expect(queryClient.isMutating({ mutationKey: orpc.ping.key({ type: 'mutation' }) })).toEqual(1) + + await vi.waitFor(() => expect(result.current.data).toEqual('pong')) + }) +}) + +describe('useSuspenseQuery', () => { + it('works - onSuccess', async () => { + const { result } = renderHook(() => useSuspenseQuery(orpc.ping.queryOptions(), queryClient)) + + expect(queryClient.isFetching({ queryKey: orpc.key() })).toEqual(1) + expect(queryClient.isFetching({ queryKey: orpc.ping.key() })).toEqual(1) + expect(queryClient.isFetching({ queryKey: orpc.ping.key({ type: 'mutation' }) })).toEqual(0) + + await vi.waitFor(() => expect(result.current.data).toEqual('pong')) + + expect(queryClient.getQueryData(orpc.ping.key({ type: 'query' }))).toEqual('pong') + }) +}) + +describe('useSuspenseInfiniteQuery', () => { + it('works - onSuccess', async () => { + const { result } = renderHook(() => useSuspenseInfiniteQuery(orpc.user.list.infiniteOptions({ + input: {}, + getNextPageParam: lastPage => lastPage.nextCursor, + }), queryClient)) + + await vi.waitFor(() => expect(result.current.data?.pages.length).toEqual(1)) + + result.current.fetchNextPage() + + await vi.waitFor(() => expect(result.current.data?.pages.length).toEqual(2)) + }) +}) diff --git a/packages/react-query/tests/helpers.tsx b/packages/react-query/tests/helpers.tsx new file mode 100644 index 00000000..6a8b9fe4 --- /dev/null +++ b/packages/react-query/tests/helpers.tsx @@ -0,0 +1,117 @@ +import { createORPCClient } from '@orpc/client' +import { os } from '@orpc/server' +import { createORPCHandler, handleFetchRequest } from '@orpc/server/fetch' +import { QueryClient } from '@tanstack/react-query' +import { z } from 'zod' +import { createORPCReactQueryUtils } from '../src' + +export const orpcServer = os + +export const ping = orpcServer.func(() => 'pong') + +export const UserSchema = z + .object({ data: z.object({ id: z.string(), name: z.string() }) }) + .transform(data => data.data) +export const UserFindInputSchema = z + .object({ id: z.string() }) + .transform(data => ({ data })) + +export const userFind = orpcServer + .input(UserFindInputSchema) + .output(UserSchema) + .func((input) => { + return { + data: { + id: input.data.id, + name: `name-${input.data.id}`, + }, + } + }) + +export const UserListInputSchema = z + .object({ + keyword: z.string().optional(), + cursor: z.number().default(0), + }) + .transform(data => ({ data })) +export const UserListOutputSchema = z + .object({ + data: z.object({ + nextCursor: z.number(), + users: z.array(UserSchema), + }), + }) + .transform(data => data.data) +export const userList = orpcServer + .input(UserListInputSchema) + .output(UserListOutputSchema) + .func((input) => { + return { + data: { + nextCursor: input.data.cursor + 2, + users: [ + { + data: { + id: `id-${input.data.cursor}`, + name: `number-${input.data.cursor}`, + }, + }, + { + data: { + id: `id-${input.data.cursor + 1}`, + name: `number-${input.data.cursor + 1}`, + }, + }, + ], + }, + } + }) + +export const UserCreateInputSchema = z + .object({ name: z.string() }) + .transform(data => ({ data })) +export const userCreate = orpcServer + .input(UserCreateInputSchema) + .output(UserSchema) + .func((input) => { + return { + data: { + id: '28aa6286-48e9-4f23-adea-3486c86acd55', + name: input.data.name, + }, + } + }) + +export const appRouter = orpcServer.router({ + ping, + user: { + find: userFind, + list: userList, + create: userCreate, + }, +}) + +export const orpcClient = createORPCClient({ + baseURL: 'http://localhost:3000', + + async fetch(...args) { + await new Promise(resolve => setTimeout(resolve, 100)) + const request = new Request(...args) + + return handleFetchRequest({ + router: appRouter, + request, + handlers: [createORPCHandler()], + }) + }, +}) + +export const orpc = createORPCReactQueryUtils(orpcClient) + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, +}) diff --git a/vitest.workspace.ts b/vitest.workspace.ts index f26b0e01..52e144b7 100644 --- a/vitest.workspace.ts +++ b/vitest.workspace.ts @@ -19,7 +19,7 @@ export default defineWorkspace([ globals: true, environment: 'jsdom', setupFiles: ['./vitest.jsdom-react.ts'], - include: ['./packages/{next,react}/**/*.test.tsx'], + include: ['./packages/next/**/*.test.tsx', './packages/react-query/**/*.test.tsx'], }, }, ]) From c964092b57197404b8e3de1036cfe10b746fb6e3 Mon Sep 17 00:00:00 2001 From: unnoq Date: Mon, 9 Dec 2024 20:16:17 +0700 Subject: [PATCH 07/17] remove prefix --- packages/react-query/src/key.test.ts | 10 ++++---- packages/react-query/src/key.ts | 3 +-- .../react-query/src/utils-general.test.ts | 4 ++-- packages/react-query/src/utils-general.ts | 3 +-- .../react-query/src/utils-procedure.test-d.ts | 18 +++++++------- .../react-query/src/utils-procedure.test.ts | 12 +++++----- packages/react-query/src/utils-procedure.ts | 7 +++--- .../react-query/src/utils-router.test-d.ts | 20 ++++++++-------- packages/react-query/src/utils-router.test.ts | 24 +++++++++---------- packages/react-query/src/utils-router.ts | 10 ++++---- 10 files changed, 53 insertions(+), 58 deletions(-) diff --git a/packages/react-query/src/key.test.ts b/packages/react-query/src/key.test.ts index a9984028..de7f32e7 100644 --- a/packages/react-query/src/key.test.ts +++ b/packages/react-query/src/key.test.ts @@ -2,16 +2,16 @@ import { buildKey } from './key' describe('buildKey', () => { it('works', () => { - expect(buildKey('__ORPC__', ['path'])).toEqual(['__ORPC__', ['path'], {}]) - expect(buildKey('__ORPC__', ['path'], { type: 'query' })).toEqual(['__ORPC__', ['path'], { type: 'query' }]) + expect(buildKey(['path'])).toEqual(['__ORPC__', ['path'], {}]) + expect(buildKey(['path'], { type: 'query' })).toEqual(['__ORPC__', ['path'], { type: 'query' }]) - expect(buildKey('__ORPC__', ['path'], { type: 'query', input: { a: 1 } })) + expect(buildKey(['path'], { type: 'query', input: { a: 1 } })) .toEqual(['__ORPC__', ['path'], { type: 'query', input: { a: 1 } }]) - expect(buildKey('__ORPC__', ['path'], { type: 'query', input: undefined })) + expect(buildKey(['path'], { type: 'query', input: undefined })) .toEqual(['__ORPC__', ['path'], { type: 'query' }]) - expect(buildKey('__ORPC__', ['path'], { type: undefined, input: undefined })) + expect(buildKey(['path'], { type: undefined, input: undefined })) .toEqual(['__ORPC__', ['path'], { }]) }) }) diff --git a/packages/react-query/src/key.ts b/packages/react-query/src/key.ts index 6a54a016..f4c61c84 100644 --- a/packages/react-query/src/key.ts +++ b/packages/react-query/src/key.ts @@ -9,7 +9,6 @@ export interface BuildKeyOptions { } export function buildKey( - prefix: string, path: string[], options?: BuildKeyOptions, ): QueryKey { @@ -18,7 +17,7 @@ export function buildKey( const withType = options?.type !== undefined ? { type: options?.type } : {} return [ - prefix, + '__ORPC__', path, { ...withInput, diff --git a/packages/react-query/src/utils-general.test.ts b/packages/react-query/src/utils-general.test.ts index d14ce098..0d43971e 100644 --- a/packages/react-query/src/utils-general.test.ts +++ b/packages/react-query/src/utils-general.test.ts @@ -9,9 +9,9 @@ beforeEach(() => { describe('key', () => { it('works', () => { - const utils = createGeneralUtils('__ORPC__', ['path']) + const utils = createGeneralUtils(['path']) expect(utils.key({ input: 'input', type: 'infinite' })).toEqual(['__ORPC__', ['path'], { input: 'input', type: 'infinite' }]) expect(buildKeySpy).toHaveBeenCalledTimes(1) - expect(buildKeySpy).toHaveBeenCalledWith('__ORPC__', ['path'], { input: 'input', type: 'infinite' }) + expect(buildKeySpy).toHaveBeenCalledWith(['path'], { input: 'input', type: 'infinite' }) }) }) diff --git a/packages/react-query/src/utils-general.ts b/packages/react-query/src/utils-general.ts index dc4ef7a7..934347de 100644 --- a/packages/react-query/src/utils-general.ts +++ b/packages/react-query/src/utils-general.ts @@ -10,12 +10,11 @@ export interface GeneralUtils { } export function createGeneralUtils( - prefix: string, path: string[], ): GeneralUtils { return { key(options) { - return buildKey(prefix, path, options) + return buildKey(path, options) }, } } diff --git a/packages/react-query/src/utils-procedure.test-d.ts b/packages/react-query/src/utils-procedure.test-d.ts index 768d2472..6bd5465f 100644 --- a/packages/react-query/src/utils-procedure.test-d.ts +++ b/packages/react-query/src/utils-procedure.test-d.ts @@ -4,10 +4,10 @@ import { createProcedureUtils } from './utils-procedure' describe('queryOptions', () => { const client = vi.fn((input: number | undefined) => Promise.resolve(input?.toString())) - const utils = createProcedureUtils(client, '__ORPC__', []) + const utils = createProcedureUtils(client, []) const client2 = vi.fn((input: number) => Promise.resolve(input.toString())) - const utils2 = createProcedureUtils(client2, '__ORPC__', []) + const utils2 = createProcedureUtils(client2, []) it('infer correct input type', () => { utils.queryOptions({ input: 1 }) @@ -51,7 +51,7 @@ describe('infiniteOptions', () => { const getNextPageParam = vi.fn() it('cannot use on procedure without input object-able', () => { - const utils = createProcedureUtils({} as (input: number) => Promise, '__ORPC__', []) + const utils = createProcedureUtils({} as (input: number) => Promise, []) // @ts-expect-error missing initialPageParam utils.infiniteOptions({ @@ -74,7 +74,7 @@ describe('infiniteOptions', () => { }) it('infer correct input type', () => { - const utils = createProcedureUtils({} as (input: { limit?: number, cursor: number }) => Promise, '__ORPC__', []) + const utils = createProcedureUtils({} as (input: { limit?: number, cursor: number }) => Promise, []) utils.infiniteOptions({ input: { @@ -105,7 +105,7 @@ describe('infiniteOptions', () => { }) it('infer correct initialPageParam type', () => { - const utils = createProcedureUtils({} as (input: { limit?: number, cursor: number }) => Promise, '__ORPC__', []) + const utils = createProcedureUtils({} as (input: { limit?: number, cursor: number }) => Promise, []) utils.infiniteOptions({ input: {}, @@ -128,7 +128,7 @@ describe('infiniteOptions', () => { }) it('initialPageParam can be optional', () => { - const utils = createProcedureUtils({} as (input: { limit?: number, cursor?: number }) => Promise, '__ORPC__', []) + const utils = createProcedureUtils({} as (input: { limit?: number, cursor?: number }) => Promise, []) utils.infiniteOptions({ input: {}, @@ -137,7 +137,7 @@ describe('infiniteOptions', () => { }) it('infer correct output type', () => { - const utils = createProcedureUtils({} as (input: { limit?: number, cursor: number }) => Promise, '__ORPC__', []) + const utils = createProcedureUtils({} as (input: { limit?: number, cursor: number }) => Promise, []) const query = useInfiniteQuery(utils.infiniteOptions({ input: { limit: 1, @@ -152,7 +152,7 @@ describe('infiniteOptions', () => { }) it('work with select options', () => { - const utils = createProcedureUtils({} as (input: { limit?: number, cursor: number }) => Promise, '__ORPC__', []) + const utils = createProcedureUtils({} as (input: { limit?: number, cursor: number }) => Promise, []) const query = useInfiniteQuery(utils.infiniteOptions({ input: { limit: 1, @@ -174,7 +174,7 @@ describe('infiniteOptions', () => { describe('mutationOptions', () => { const client = vi.fn((input: number) => Promise.resolve(input.toString())) - const utils = createProcedureUtils(client, '__ORPC__', []) + const utils = createProcedureUtils(client, []) it('infer correct input type', () => { const option = utils.mutationOptions({ diff --git a/packages/react-query/src/utils-procedure.test.ts b/packages/react-query/src/utils-procedure.test.ts index 0520847d..bfa12c58 100644 --- a/packages/react-query/src/utils-procedure.test.ts +++ b/packages/react-query/src/utils-procedure.test.ts @@ -9,14 +9,14 @@ beforeEach(() => { describe('queryOptions', () => { const client = vi.fn((input: number | undefined) => Promise.resolve(input?.toString())) - const utils = createProcedureUtils(client, '__ORPC__', ['ping']) + const utils = createProcedureUtils(client, ['ping']) it('works', async () => { const options = utils.queryOptions({ input: 1 }) expect(options.queryKey).toEqual(['__ORPC__', ['ping'], { type: 'query', input: 1 }]) expect(buildKeySpy).toHaveBeenCalledTimes(1) - expect(buildKeySpy).toHaveBeenCalledWith('__ORPC__', ['ping'], { type: 'query', input: 1 }) + expect(buildKeySpy).toHaveBeenCalledWith(['ping'], { type: 'query', input: 1 }) client.mockResolvedValueOnce('__mocked__') await expect((options as any).queryFn()).resolves.toEqual('__mocked__') @@ -30,7 +30,7 @@ describe('infiniteOptions', () => { it('works ', async () => { const client = vi.fn<(input: { limit?: number, cursor: number | undefined }) => Promise>() - const utils = createProcedureUtils(client, '__ORPC__', []) + const utils = createProcedureUtils(client, []) const options = utils.infiniteOptions({ input: { limit: 5 }, @@ -41,7 +41,7 @@ describe('infiniteOptions', () => { expect(options.initialPageParam).toEqual(1) expect(options.queryKey).toEqual(['__ORPC__', [], { type: 'infinite', input: { limit: 5 } }]) expect(buildKeySpy).toHaveBeenCalledTimes(1) - expect(buildKeySpy).toHaveBeenCalledWith('__ORPC__', [], { type: 'infinite', input: { limit: 5 } }) + expect(buildKeySpy).toHaveBeenCalledWith([], { type: 'infinite', input: { limit: 5 } }) client.mockResolvedValueOnce('__mocked__') await expect((options as any).queryFn({ pageParam: 1 })).resolves.toEqual('__mocked__') @@ -51,7 +51,7 @@ describe('infiniteOptions', () => { it('works without initialPageParam', async () => { const client = vi.fn<(input: { limit?: number, cursor: number | undefined }) => Promise>() - const utils = createProcedureUtils(client, '__ORPC__', []) + const utils = createProcedureUtils(client, []) const options = utils.infiniteOptions({ input: { limit: 5 }, @@ -60,7 +60,7 @@ describe('infiniteOptions', () => { expect(options.queryKey).toEqual(['__ORPC__', [], { type: 'infinite', input: { limit: 5 } }]) expect(buildKeySpy).toHaveBeenCalledTimes(1) - expect(buildKeySpy).toHaveBeenCalledWith('__ORPC__', [], { type: 'infinite', input: { limit: 5 } }) + expect(buildKeySpy).toHaveBeenCalledWith([], { type: 'infinite', input: { limit: 5 } }) client.mockResolvedValueOnce('__mocked__') await expect((options as any).queryFn({ pageParam: undefined })).resolves.toEqual('__mocked__') diff --git a/packages/react-query/src/utils-procedure.ts b/packages/react-query/src/utils-procedure.ts index 1c39c3b6..27527d2b 100644 --- a/packages/react-query/src/utils-procedure.ts +++ b/packages/react-query/src/utils-procedure.ts @@ -32,7 +32,6 @@ export interface ProcedureUtils { export function createProcedureUtils( client: (input: TInput) => Promise, - prefix: string, path: string[], ): ProcedureUtils { return { @@ -40,7 +39,7 @@ export function createProcedureUtils( const input = options?.input return { - queryKey: buildKey(prefix, path, { type: 'query', input }), + queryKey: buildKey(path, { type: 'query', input }), queryFn: () => client(input as TInput), ...options, } @@ -50,7 +49,7 @@ export function createProcedureUtils( const input = options.input return { - queryKey: buildKey(prefix, path, { type: 'infinite', input }), + queryKey: buildKey(path, { type: 'infinite', input }), queryFn: ({ pageParam }) => client({ ...(input as any), cursor: pageParam }), ...(options as any), } @@ -58,7 +57,7 @@ export function createProcedureUtils( mutationOptions(options) { return { - mutationKey: buildKey(prefix, path, { type: 'mutation' }), + mutationKey: buildKey(path, { type: 'mutation' }), mutationFn: input => client(input), ...options, } diff --git a/packages/react-query/src/utils-router.test-d.ts b/packages/react-query/src/utils-router.test-d.ts index 1918b184..41d89c73 100644 --- a/packages/react-query/src/utils-router.test-d.ts +++ b/packages/react-query/src/utils-router.test-d.ts @@ -24,11 +24,11 @@ describe('with contract router', () => { it('build correct types', () => { const utils = createRouterUtils({} as any) - const generalUtils = createGeneralUtils('__ORPC__', []) - const pingUtils = createProcedureUtils(ping, '__ORPC__', []) - const pingGeneralUtils = createGeneralUtils<{ name: string }>('__ORPC__', ['ping']) - const pongUtils = createProcedureUtils(pong, '__ORPC__', []) - const pongGeneralUtils = createGeneralUtils('__ORPC__', ['ping']) + const generalUtils = createGeneralUtils([]) + const pingUtils = createProcedureUtils(ping, []) + const pingGeneralUtils = createGeneralUtils<{ name: string }>(['ping']) + const pongUtils = createProcedureUtils(pong, []) + const pongGeneralUtils = createGeneralUtils(['ping']) expectTypeOf(utils).toMatchTypeOf() expectTypeOf(utils.ping).toMatchTypeOf() @@ -42,11 +42,11 @@ describe('with router', () => { it('build correct types', () => { const utils = createRouterUtils({} as any) - const generalUtils = createGeneralUtils('__ORPC__', []) - const pingUtils = createProcedureUtils(ping, '__ORPC__', []) - const pingGeneralUtils = createGeneralUtils<{ name: string }>('__ORPC__', ['ping']) - const pongUtils = createProcedureUtils(pong, '__ORPC__', []) - const pongGeneralUtils = createGeneralUtils('__ORPC__', ['ping']) + const generalUtils = createGeneralUtils([]) + const pingUtils = createProcedureUtils(ping, []) + const pingGeneralUtils = createGeneralUtils<{ name: string }>(['ping']) + const pongUtils = createProcedureUtils(pong, []) + const pongGeneralUtils = createGeneralUtils(['ping']) expectTypeOf(utils).toMatchTypeOf() expectTypeOf(utils.ping).toMatchTypeOf() diff --git a/packages/react-query/src/utils-router.test.ts b/packages/react-query/src/utils-router.test.ts index 5623aba2..ecbb2bcb 100644 --- a/packages/react-query/src/utils-router.test.ts +++ b/packages/react-query/src/utils-router.test.ts @@ -19,9 +19,9 @@ describe('router utils', () => { const utils = createRouterUtils(client) as any expect(generalUtilsSpy).toHaveBeenCalledTimes(1) - expect(generalUtilsSpy).toHaveBeenCalledWith('__oRPC__', []) + expect(generalUtilsSpy).toHaveBeenCalledWith([]) expect(procedureUtilsSpy).toHaveBeenCalledTimes(1) - expect(procedureUtilsSpy).toHaveBeenCalledWith(client, '__oRPC__', []) + expect(procedureUtilsSpy).toHaveBeenCalledWith(client, []) expect(typeof utils.key).toEqual('function') expect(typeof utils.queryOptions).toEqual('function') @@ -31,9 +31,9 @@ describe('router utils', () => { void utils.ping expect(generalUtilsSpy).toHaveBeenCalledTimes(1) - expect(generalUtilsSpy).toHaveBeenCalledWith('__oRPC__', ['ping']) + expect(generalUtilsSpy).toHaveBeenCalledWith(['ping']) expect(procedureUtilsSpy).toHaveBeenCalledTimes(1) - expect(procedureUtilsSpy).toHaveBeenCalledWith(client.ping, '__oRPC__', ['ping']) + expect(procedureUtilsSpy).toHaveBeenCalledWith(client.ping, ['ping']) expect(typeof utils.ping.key).toEqual('function') expect(typeof utils.ping.queryOptions).toEqual('function') @@ -43,25 +43,25 @@ describe('router utils', () => { void utils.ping.peng expect(generalUtilsSpy).toHaveBeenCalledTimes(2) - expect(generalUtilsSpy).toHaveBeenNthCalledWith(1, '__oRPC__', ['ping']) - expect(generalUtilsSpy).toHaveBeenNthCalledWith(2, '__oRPC__', ['ping', 'peng']) + expect(generalUtilsSpy).toHaveBeenNthCalledWith(1, ['ping']) + expect(generalUtilsSpy).toHaveBeenNthCalledWith(2, ['ping', 'peng']) expect(procedureUtilsSpy).toHaveBeenCalledTimes(2) - expect(procedureUtilsSpy).toHaveBeenNthCalledWith(1, client.ping, '__oRPC__', ['ping']) - expect(procedureUtilsSpy).toHaveBeenNthCalledWith(2, client.ping.peng, '__oRPC__', ['ping', 'peng']) + expect(procedureUtilsSpy).toHaveBeenNthCalledWith(1, client.ping, ['ping']) + expect(procedureUtilsSpy).toHaveBeenNthCalledWith(2, client.ping.peng, ['ping', 'peng']) expect(typeof utils.ping.peng.key).toEqual('function') expect(typeof utils.ping.peng.queryOptions).toEqual('function') }) - it('can custom prefix and base path', () => { + it('can custom base path', () => { const client = vi.fn() as any - const utils = createRouterUtils(client, 'prefix', ['base']) as any + const utils = createRouterUtils(client, ['base']) as any expect(generalUtilsSpy).toHaveBeenCalledTimes(1) - expect(generalUtilsSpy).toHaveBeenCalledWith('prefix', ['base']) + expect(generalUtilsSpy).toHaveBeenCalledWith(['base']) expect(procedureUtilsSpy).toHaveBeenCalledTimes(1) - expect(procedureUtilsSpy).toHaveBeenCalledWith(client, 'prefix', ['base']) + expect(procedureUtilsSpy).toHaveBeenCalledWith(client, ['base']) }) }) diff --git a/packages/react-query/src/utils-router.ts b/packages/react-query/src/utils-router.ts index 3ec6ab43..9f427983 100644 --- a/packages/react-query/src/utils-router.ts +++ b/packages/react-query/src/utils-router.ts @@ -18,16 +18,14 @@ export type RouterUtils | ContractRouter> = { /** * @param client - The client create form `@orpc/client` - * @param prefix - Prefix query, mutation key help you prevent conflict with other query or mutation - * @param path - The path of the procedure + * @param path - The base path for query key */ export function createRouterUtils | ContractRouter>( client: any, // TODO typed - prefix: string = '__oRPC__', path: string[] = [], ): RouterUtils { - const generalUtils = createGeneralUtils(prefix, path) - const procedureUtils = createProcedureUtils(client, prefix, path) + const generalUtils = createGeneralUtils(path) + const procedureUtils = createProcedureUtils(client, path) const recursive = new Proxy({ ...generalUtils, @@ -40,7 +38,7 @@ export function createRouterUtils | ContractRouter>( return value } - const nextUtils = createRouterUtils(client[prop], prefix, [...path, prop]) + const nextUtils = createRouterUtils(client[prop], [...path, prop]) if (typeof value !== 'function') { return nextUtils From 9910285a4f037c8bacfc954f0354f42051dea8f4 Mon Sep 17 00:00:00 2001 From: unnoq Date: Tue, 10 Dec 2024 08:29:16 +0700 Subject: [PATCH 08/17] fix suspense issues --- packages/react-query/src/types.ts | 25 +++++----- .../react-query/src/utils-general.test-d.ts | 2 +- .../react-query/src/utils-procedure.test-d.ts | 4 +- packages/react-query/src/utils-procedure.ts | 46 +++++++++---------- 4 files changed, 38 insertions(+), 39 deletions(-) diff --git a/packages/react-query/src/types.ts b/packages/react-query/src/types.ts index 7bd9c425..02aca6ba 100644 --- a/packages/react-query/src/types.ts +++ b/packages/react-query/src/types.ts @@ -1,20 +1,23 @@ +import type { SetOptional } from '@orpc/shared' import type { DefaultError, QueryKey, - UseInfiniteQueryOptions, + UndefinedInitialDataInfiniteOptions, + UndefinedInitialDataOptions, UseMutationOptions, - UseQueryOptions, } from '@tanstack/react-query' -export interface QueryOptions extends UseQueryOptions { - input: TInput -} +export type InferCursor = T extends { cursor?: any } ? T['cursor'] : never -export interface InfiniteOptions extends UseInfiniteQueryOptions { - input: TInput -} +export type QueryOptions = (undefined extends TInput ? { input?: TInput } : { input: TInput }) & ( + SetOptional, 'queryKey'> +) -export interface MutationOptions extends UseMutationOptions { -} +export type InfiniteOptions = { input: Omit } & ( + SetOptional< + UndefinedInitialDataInfiniteOptions>, + 'queryKey' | (undefined extends InferCursor ? 'initialPageParam' : never) + > +) -export type InferCursor = T extends { cursor?: any } ? T['cursor'] : never +export type MutationOptions = UseMutationOptions diff --git a/packages/react-query/src/utils-general.test-d.ts b/packages/react-query/src/utils-general.test-d.ts index ba5f6c95..1d0210c9 100644 --- a/packages/react-query/src/utils-general.test-d.ts +++ b/packages/react-query/src/utils-general.test-d.ts @@ -1,7 +1,7 @@ import { createGeneralUtils } from './utils-general' describe('key', () => { - const utils = createGeneralUtils<{ a: { b: { c: number } } }>('__ORPC__', []) + const utils = createGeneralUtils<{ a: { b: { c: number } } }>([]) it('infer correct input type & partial input', () => { utils.key() diff --git a/packages/react-query/src/utils-procedure.test-d.ts b/packages/react-query/src/utils-procedure.test-d.ts index 6bd5465f..23cb315c 100644 --- a/packages/react-query/src/utils-procedure.test-d.ts +++ b/packages/react-query/src/utils-procedure.test-d.ts @@ -142,12 +142,12 @@ describe('infiniteOptions', () => { input: { limit: 1, }, - getNextPageParam, + getNextPageParam: () => 1, initialPageParam: 1, })) if (query.status === 'success') { - expectTypeOf(query.data).toEqualTypeOf>() + expectTypeOf(query.data).toEqualTypeOf>() } }) diff --git a/packages/react-query/src/utils-procedure.ts b/packages/react-query/src/utils-procedure.ts index 27527d2b..2794e9ab 100644 --- a/packages/react-query/src/utils-procedure.ts +++ b/packages/react-query/src/utils-procedure.ts @@ -1,29 +1,21 @@ -import type { PartialOnUndefinedDeep, SetOptional } from '@orpc/shared' -import type { DefaultError, InfiniteData, QueryKey, UseInfiniteQueryOptions, UseMutationOptions, UseQueryOptions } from '@tanstack/react-query' -import type { InferCursor } from './types' +import type { IsEqual, SetOptional } from '@orpc/shared' +import type { DefaultError, QueryKey, UseMutationOptions } from '@tanstack/react-query' +import type { InfiniteOptions, QueryOptions } from './types' import { buildKey } from './key' /** * Utils at procedure level */ export interface ProcedureUtils { - queryOptions: ( - ...options: [ - & SetOptional, 'queryFn' | 'queryKey'> - & PartialOnUndefinedDeep<{ input: TInput }>, - ] | (undefined extends TInput ? [] : never) - ) => UseQueryOptions + queryOptions: >( + ...options: [U] | (undefined extends TInput ? [] : never) + ) => IsEqual> extends true + ? { queryKey: QueryKey, queryFn: () => Promise } + : Omit<{ queryKey: QueryKey, queryFn: () => Promise }, keyof U> & U - infiniteOptions: >>( - options: - & SetOptional< - PartialOnUndefinedDeep< - UseInfiniteQueryOptions> - >, - 'queryFn' | 'queryKey' - > - & PartialOnUndefinedDeep<{ input: Omit }> - ) => UseInfiniteQueryOptions> + infiniteOptions: >( + options: U + ) => Omit<{ queryKey: QueryKey, queryFn: () => Promise, initialPageParam: undefined }, keyof U> & U mutationOptions: ( options?: SetOptional, 'mutationFn' | 'mutationKey'> @@ -36,23 +28,27 @@ export function createProcedureUtils( ): ProcedureUtils { return { queryOptions(...[options]) { - const input = options?.input + const input = options?.input as any - return { + const result = { queryKey: buildKey(path, { type: 'query', input }), - queryFn: () => client(input as TInput), + queryFn: () => client(input), ...options, } + + return result as any }, infiniteOptions(options) { - const input = options.input + const input = options.input as any - return { + const result = { queryKey: buildKey(path, { type: 'infinite', input }), - queryFn: ({ pageParam }) => client({ ...(input as any), cursor: pageParam }), + queryFn: ({ pageParam }: { pageParam: any }) => client({ ...input, cursor: pageParam }), ...(options as any), } + + return result }, mutationOptions(options) { From e1c5c3cb19f6cc37c9781e54013087651c698f6b Mon Sep 17 00:00:00 2001 From: unnoq Date: Tue, 10 Dec 2024 08:39:48 +0700 Subject: [PATCH 09/17] optional input when possible --- packages/react-query/src/types.ts | 2 +- .../react-query/src/utils-procedure.test-d.ts | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/react-query/src/types.ts b/packages/react-query/src/types.ts index 02aca6ba..d103ff6c 100644 --- a/packages/react-query/src/types.ts +++ b/packages/react-query/src/types.ts @@ -13,7 +13,7 @@ export type QueryOptions = (undefined extends TInp SetOptional, 'queryKey'> ) -export type InfiniteOptions = { input: Omit } & ( +export type InfiniteOptions = (undefined extends TInput ? { input?: Omit } : { input: Omit }) & ( SetOptional< UndefinedInitialDataInfiniteOptions>, 'queryKey' | (undefined extends InferCursor ? 'initialPageParam' : never) diff --git a/packages/react-query/src/utils-procedure.test-d.ts b/packages/react-query/src/utils-procedure.test-d.ts index 23cb315c..0f56aa02 100644 --- a/packages/react-query/src/utils-procedure.test-d.ts +++ b/packages/react-query/src/utils-procedure.test-d.ts @@ -134,6 +134,29 @@ describe('infiniteOptions', () => { input: {}, getNextPageParam, }) + + const utils2 = createProcedureUtils({} as (input: { limit?: number, cursor: number }) => Promise, []) + + // @ts-expect-error initialPageParam is required + utils2.infiniteOptions({ + input: {}, + getNextPageParam, + }) + }) + + it('input can be optional', () => { + const utils = createProcedureUtils({} as (input: { limit?: number, cursor?: number } | undefined) => Promise, []) + + utils.infiniteOptions({ + getNextPageParam, + }) + + const utils2 = createProcedureUtils({} as (input: { limit?: number, cursor?: number }) => Promise, []) + + // @ts-expect-error input is required + utils2.infiniteOptions({ + getNextPageParam, + }) }) it('infer correct output type', () => { From 80d41ca9f4bbc917f2bfd97bb3c5b4bd67c080e7 Mon Sep 17 00:00:00 2001 From: unnoq Date: Tue, 10 Dec 2024 10:50:48 +0700 Subject: [PATCH 10/17] docs --- .../content/docs/client/react-query.mdx | 172 ++++++++++++++++++ apps/content/examples/react-query.ts | 4 + apps/content/package.json | 1 + apps/content/tsconfig.json | 1 + 4 files changed, 178 insertions(+) create mode 100644 apps/content/content/docs/client/react-query.mdx create mode 100644 apps/content/examples/react-query.ts diff --git a/apps/content/content/docs/client/react-query.mdx b/apps/content/content/docs/client/react-query.mdx new file mode 100644 index 00000000..c22b7a8a --- /dev/null +++ b/apps/content/content/docs/client/react-query.mdx @@ -0,0 +1,172 @@ +--- +title: React Query +description: Simplify React Query usage with minimal integration using ORPC and TanStack Query +--- + +## Installation + +```package-install +@orpc/client @orpc/react-query @tanstack/react-query +``` + +## Setup + +### Using a Global Client + +```ts twoslash +import { createORPCReactQueryUtils } from '@orpc/react-query'; +import { createORPCClient } from '@orpc/client'; +import type { router } from 'examples/server'; + +// Create an ORPC client +export const client = createORPCClient({ + baseURL: 'http://localhost:3000/api', +}); + +// Create React Query utilities for ORPC +export const orpc = createORPCReactQueryUtils(client); + +// @noErrors +orpc.getting. +// ^| +``` + +### Using Context with a Client + +```tsx twoslash +import { createORPCReactQueryUtils, RouterUtils } from '@orpc/react-query'; +import { createORPCClient } from '@orpc/client'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import type { router } from 'examples/server'; +import * as React from 'react'; + +const ORPCContext = React.createContext | undefined>(undefined); + +export function useORPC() { + const orpc = React.useContext(ORPCContext); + + if (!orpc) { + throw new Error('ORPCContext is not available.'); + } + + return orpc; +} + +export function ORPCProvider({ children }: { children: React.ReactNode }) { + const [client] = React.useState(() => + createORPCClient({ + baseURL: 'http://localhost:3000/api', + }) + ); + const [queryClient] = React.useState(() => new QueryClient()); + + const orpc = React.useMemo(() => createORPCReactQueryUtils(client), [client]); + + return ( + + + {children} + + + ); +} + +// Example usage +const orpc = useORPC(); +// @noErrors +orpc.getting. +// ^| +``` + +## Multiple ORPC Instances + +To prevent conflicts when using multiple ORPC instances, you can provide a unique base path to `createORPCReactQueryUtils`. + +```tsx twoslash +import { createORPCReactQueryUtils } from '@orpc/react-query' + +// Create separate ORPC instances with unique base paths +const userORPC = createORPCReactQueryUtils('fake-client' as any, ['__user-api__']) +const postORPC = createORPCReactQueryUtils('fake-client' as any, ['__post-api__']) +``` + +This ensures that each instance manages its own set of query keys and avoids any conflicts. + +## Usage + +### Standard Queries and Mutations + +```ts twoslash +import { useMutation, useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query' +import { orpc } from 'examples/react-query' + +// Fetch data +const { data: gettingData } = useQuery(orpc.getting.queryOptions({ input: { name: 'unnoq' } })) + +// Use suspense query +const { data: postData } = useSuspenseQuery( + orpc.post.find.queryOptions({ input: { id: 'example' } }), +) + +// Perform mutations +const { mutate: postMutate } = useMutation(orpc.post.create.mutationOptions()) + +// Invalidate queries +const queryClient = useQueryClient() +queryClient.invalidateQueries({ queryKey: orpc.key() }) // Invalidate all queries +queryClient.invalidateQueries({ queryKey: orpc.post.find.key({ input: { id: 'example' } }) }) // Specific queries +``` + +> **Note**: This library enhances [TanStack Query](https://tanstack.com/query/latest) by managing query keys and functions for you, providing a seamless developer experience. + + +## Infinite Queries + +Infinite queries require a `cursor` in the input field for pagination. + +```tsx twoslash +import { os } from '@orpc/server'; +import { z } from 'zod'; +import { createORPCReactQueryUtils } from '@orpc/react-query'; +import { useInfiniteQuery, useSuspenseInfiniteQuery } from '@tanstack/react-query'; +import * as React from 'react'; + +const router = { + user: { + list: os + .input(z.object({ cursor: z.number(), limit: z.number() })) + .func((input) => ({ + nextCursor: input.cursor + input.limit, + users: [], // Fetch your actual data here + })), + }, +}; + +const orpc = createORPCReactQueryUtils('fake-client' as any); + +export function MyComponent() { + const query = useInfiniteQuery( + orpc.user.list.infiniteOptions({ + input: { limit: 10 }, + getNextPageParam: (lastPage) => lastPage.nextCursor, + initialPageParam: 0, + }) + ); + + const query2 = useSuspenseInfiniteQuery( + orpc.user.list.infiniteOptions({ + input: { limit: 10 }, + getNextPageParam: (lastPage) => lastPage.nextCursor, + initialPageParam: 0, + }) + ); + + return ( +
+ {query.isLoading && 'Loading...'} + {query.isSuccess &&
Data: {JSON.stringify(query.data)}
} + {query.isError &&
Error: {query.error.message}
} +
+ ); +} +``` diff --git a/apps/content/examples/react-query.ts b/apps/content/examples/react-query.ts new file mode 100644 index 00000000..d3b20b7c --- /dev/null +++ b/apps/content/examples/react-query.ts @@ -0,0 +1,4 @@ +import type { router } from 'examples/server' +import { createORPCReactQueryUtils } from '@orpc/react-query' + +export const orpc = createORPCReactQueryUtils('fake-client' as any) diff --git a/apps/content/package.json b/apps/content/package.json index 15d3eb1e..69e77b57 100644 --- a/apps/content/package.json +++ b/apps/content/package.json @@ -26,6 +26,7 @@ "@orpc/next": "workspace:*", "@orpc/openapi": "workspace:*", "@orpc/react": "workspace:*", + "@orpc/react-query": "workspace:*", "@orpc/server": "workspace:*", "@orpc/zod": "workspace:*", "@types/express": "^5.0.0", diff --git a/apps/content/tsconfig.json b/apps/content/tsconfig.json index cdb1a047..fb4c05b4 100644 --- a/apps/content/tsconfig.json +++ b/apps/content/tsconfig.json @@ -30,6 +30,7 @@ { "path": "../../packages/server" }, { "path": "../../packages/client" }, { "path": "../../packages/react" }, + { "path": "../../packages/react-query" }, { "path": "../../packages/zod" }, { "path": "../../packages/next" } ], From a158360638119e161af57e5dda84132fed4f84e7 Mon Sep 17 00:00:00 2001 From: unnoq Date: Tue, 10 Dec 2024 10:54:26 +0700 Subject: [PATCH 11/17] docs --- apps/content/content/home/landing.mdx | 43 +++++++++++++++++---------- pnpm-lock.yaml | 3 ++ 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/apps/content/content/home/landing.mdx b/apps/content/content/home/landing.mdx index f69115b4..64edb66d 100644 --- a/apps/content/content/home/landing.mdx +++ b/apps/content/content/home/landing.mdx @@ -57,24 +57,35 @@ Our [Vanilla Client](/docs/client/vanilla) is fully typed and doesn't rely on ge ## Seamless Integration with TanStack Query ```tsx -const utils = orpc.useUtils() -const { mutate, isPending } = orpc.getting.useMutation({ - onSuccess(){ - utils.invalidate() // invalidate all queries - } -}) - -mutate({ - id: 1992n, - user: { - name: 'unnoq', - avatar: document.getElementById('avatar').files[0], - } -}) +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; + +// Fetch data with oRPC +const { data, status } = useQuery( + orpc.post.find.queryOptions({ input: { id: 'example' } }) +); + +// Perform a mutation and invalidate related queries on success +const { mutate, isPending } = useMutation( + orpc.getting.mutationOptions({ + onSuccess() { + queryClient.invalidateQueries({ + queryKey: orpc.post.find.key({ input: { id: 'example' } }), + }); + }, + }) +); + +// Execute mutation with structured input +mutate({ + id: 1992n, + user: { + name: 'unnoq', + avatar: document.getElementById('avatar').files[0], // Handle file uploads + }, +}); ``` -oRPC's [TanStack Query integration](/docs/client/react) includes over 10 hooks and 29+ utilities, -such as `useQuery`, `useSuspenseQuery`, `useMutation`, `invalidate`, and more. Absolutely everything is typesafe! +Learn more about [React Query Integration](/docs/client/react-query). ## Access via OpenAPI Standard diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d3929420..a325b06e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -102,6 +102,9 @@ importers: '@orpc/react': specifier: workspace:* version: link:../../packages/react + '@orpc/react-query': + specifier: workspace:* + version: link:../../packages/react-query '@orpc/server': specifier: workspace:* version: link:../../packages/server From e156d09cf8ed0d60af0694834a2aa13e5af01c62 Mon Sep 17 00:00:00 2001 From: unnoq Date: Tue, 10 Dec 2024 19:02:52 +0700 Subject: [PATCH 12/17] fix `queryOptions` --- .../react-query/src/utils-procedure.test-d.ts | 18 +++++++---- packages/react-query/src/utils-procedure.ts | 32 +++++++++---------- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/packages/react-query/src/utils-procedure.test-d.ts b/packages/react-query/src/utils-procedure.test-d.ts index 0f56aa02..17ca9dd8 100644 --- a/packages/react-query/src/utils-procedure.test-d.ts +++ b/packages/react-query/src/utils-procedure.test-d.ts @@ -1,4 +1,4 @@ -import type { InfiniteData } from '@tanstack/react-query' +import type { InfiniteData, QueryKey } from '@tanstack/react-query' import { useInfiniteQuery, useQuery } from '@tanstack/react-query' import { createProcedureUtils } from './utils-procedure' @@ -18,7 +18,10 @@ describe('queryOptions', () => { }) it('can be called without argument', () => { - utils.queryOptions() + const option = utils.queryOptions() + + expectTypeOf(option.queryKey).toEqualTypeOf() + expectTypeOf(option.queryFn).toEqualTypeOf<() => Promise>() // @ts-expect-error invalid is required utils2.queryOptions() }) @@ -37,12 +40,12 @@ describe('queryOptions', () => { select(data) { expectTypeOf(data).toEqualTypeOf() - return 'new' as const + return 12344 as const }, })) if (query.status === 'success') { - expectTypeOf(query.data).toEqualTypeOf<'new'>() + expectTypeOf(query.data).toEqualTypeOf<12344>() } }) }) @@ -185,12 +188,12 @@ describe('infiniteOptions', () => { select(data) { expectTypeOf(data).toEqualTypeOf>() - return 'new' as const + return { value: 'string' } }, })) if (query.status === 'success') { - expectTypeOf(query.data).toEqualTypeOf<'new'>() + expectTypeOf(query.data).toEqualTypeOf<{ value: string }>() } }) }) @@ -228,6 +231,7 @@ describe('mutationOptions', () => { it('can be called without argument', () => { const option = utils.mutationOptions() - expectTypeOf(option.mutationFn!(1)).toEqualTypeOf >() + expectTypeOf(option.mutationKey).toEqualTypeOf() + expectTypeOf(option.mutationFn).toEqualTypeOf<(input: number) => Promise>() }) }) diff --git a/packages/react-query/src/utils-procedure.ts b/packages/react-query/src/utils-procedure.ts index 2794e9ab..af93c024 100644 --- a/packages/react-query/src/utils-procedure.ts +++ b/packages/react-query/src/utils-procedure.ts @@ -1,15 +1,15 @@ -import type { IsEqual, SetOptional } from '@orpc/shared' -import type { DefaultError, QueryKey, UseMutationOptions } from '@tanstack/react-query' -import type { InfiniteOptions, QueryOptions } from './types' +import type { IsEqual } from '@orpc/shared' +import type { QueryKey } from '@tanstack/react-query' +import type { InfiniteOptions, MutationOptions, QueryOptions } from './types' import { buildKey } from './key' /** * Utils at procedure level */ export interface ProcedureUtils { - queryOptions: >( + queryOptions: >( ...options: [U] | (undefined extends TInput ? [] : never) - ) => IsEqual> extends true + ) => IsEqual> extends true ? { queryKey: QueryKey, queryFn: () => Promise } : Omit<{ queryKey: QueryKey, queryFn: () => Promise }, keyof U> & U @@ -17,9 +17,11 @@ export interface ProcedureUtils { options: U ) => Omit<{ queryKey: QueryKey, queryFn: () => Promise, initialPageParam: undefined }, keyof U> & U - mutationOptions: ( - options?: SetOptional, 'mutationFn' | 'mutationKey'> - ) => UseMutationOptions + mutationOptions: >( + options?: U + ) => IsEqual> extends true + ? { mutationKey: QueryKey, mutationFn: (input: TInput) => Promise } + : Omit<{ mutationKey: QueryKey, mutationFn: (input: TInput) => Promise }, keyof U> & U } export function createProcedureUtils( @@ -30,32 +32,28 @@ export function createProcedureUtils( queryOptions(...[options]) { const input = options?.input as any - const result = { + return { queryKey: buildKey(path, { type: 'query', input }), queryFn: () => client(input), - ...options, + ...(options as any), } - - return result as any }, infiniteOptions(options) { const input = options.input as any - const result = { + return { queryKey: buildKey(path, { type: 'infinite', input }), - queryFn: ({ pageParam }: { pageParam: any }) => client({ ...input, cursor: pageParam }), + queryFn: ({ pageParam }) => client({ ...input, cursor: pageParam }), ...(options as any), } - - return result }, mutationOptions(options) { return { mutationKey: buildKey(path, { type: 'mutation' }), mutationFn: input => client(input), - ...options, + ...(options as any), } }, } From f538bfc4ddd59c3547b7e5dc3d93b592dd153b3e Mon Sep 17 00:00:00 2001 From: unnoq Date: Tue, 10 Dec 2024 19:50:53 +0700 Subject: [PATCH 13/17] type fixed --- packages/react-query/src/utils-procedure.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-query/src/utils-procedure.ts b/packages/react-query/src/utils-procedure.ts index af93c024..104a67e0 100644 --- a/packages/react-query/src/utils-procedure.ts +++ b/packages/react-query/src/utils-procedure.ts @@ -44,7 +44,7 @@ export function createProcedureUtils( return { queryKey: buildKey(path, { type: 'infinite', input }), - queryFn: ({ pageParam }) => client({ ...input, cursor: pageParam }), + queryFn: ({ pageParam }: { pageParam: unknown }) => client({ ...input, cursor: pageParam }), ...(options as any), } }, From 69dd56b31a4fdb1247b6fe0432db51f928e2e273 Mon Sep 17 00:00:00 2001 From: unnoq Date: Wed, 11 Dec 2024 16:48:42 +0700 Subject: [PATCH 14/17] type client --- packages/react-query/src/utils-procedure.test-d.ts | 7 +++++-- packages/react-query/src/utils-procedure.test.ts | 3 ++- packages/react-query/src/utils-procedure.ts | 5 +++-- packages/react-query/src/utils-router.ts | 7 ++++--- packages/react-query/tests/helpers.tsx | 2 +- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/react-query/src/utils-procedure.test-d.ts b/packages/react-query/src/utils-procedure.test-d.ts index 17ca9dd8..c42d3ce0 100644 --- a/packages/react-query/src/utils-procedure.test-d.ts +++ b/packages/react-query/src/utils-procedure.test-d.ts @@ -1,9 +1,12 @@ +import type { Caller } from '@orpc/server' import type { InfiniteData, QueryKey } from '@tanstack/react-query' import { useInfiniteQuery, useQuery } from '@tanstack/react-query' import { createProcedureUtils } from './utils-procedure' describe('queryOptions', () => { - const client = vi.fn((input: number | undefined) => Promise.resolve(input?.toString())) + const client = vi.fn>( + (...[input]) => Promise.resolve(input?.toString()), + ) const utils = createProcedureUtils(client, []) const client2 = vi.fn((input: number) => Promise.resolve(input.toString())) @@ -148,7 +151,7 @@ describe('infiniteOptions', () => { }) it('input can be optional', () => { - const utils = createProcedureUtils({} as (input: { limit?: number, cursor?: number } | undefined) => Promise, []) + const utils = createProcedureUtils({} as Caller<{ limit?: number, cursor?: number } | undefined, string>, []) utils.infiniteOptions({ getNextPageParam, diff --git a/packages/react-query/src/utils-procedure.test.ts b/packages/react-query/src/utils-procedure.test.ts index bfa12c58..d39e6170 100644 --- a/packages/react-query/src/utils-procedure.test.ts +++ b/packages/react-query/src/utils-procedure.test.ts @@ -1,3 +1,4 @@ +import type { Caller } from '@orpc/server' import * as keyModule from './key' import { createProcedureUtils } from './utils-procedure' @@ -8,7 +9,7 @@ beforeEach(() => { }) describe('queryOptions', () => { - const client = vi.fn((input: number | undefined) => Promise.resolve(input?.toString())) + const client = vi.fn>((...[input]) => Promise.resolve(input?.toString())) const utils = createProcedureUtils(client, ['ping']) it('works', async () => { diff --git a/packages/react-query/src/utils-procedure.ts b/packages/react-query/src/utils-procedure.ts index 104a67e0..3a64169a 100644 --- a/packages/react-query/src/utils-procedure.ts +++ b/packages/react-query/src/utils-procedure.ts @@ -1,3 +1,4 @@ +import type { Caller } from '@orpc/server' import type { IsEqual } from '@orpc/shared' import type { QueryKey } from '@tanstack/react-query' import type { InfiniteOptions, MutationOptions, QueryOptions } from './types' @@ -8,7 +9,7 @@ import { buildKey } from './key' */ export interface ProcedureUtils { queryOptions: >( - ...options: [U] | (undefined extends TInput ? [] : never) + ...opts: [options: U] | (undefined extends TInput ? [] : never) ) => IsEqual> extends true ? { queryKey: QueryKey, queryFn: () => Promise } : Omit<{ queryKey: QueryKey, queryFn: () => Promise }, keyof U> & U @@ -25,7 +26,7 @@ export interface ProcedureUtils { } export function createProcedureUtils( - client: (input: TInput) => Promise, + client: Caller, path: string[], ): ProcedureUtils { return { diff --git a/packages/react-query/src/utils-router.ts b/packages/react-query/src/utils-router.ts index 9f427983..1a34a815 100644 --- a/packages/react-query/src/utils-router.ts +++ b/packages/react-query/src/utils-router.ts @@ -1,3 +1,4 @@ +import type { RouterClient } from '@orpc/client' import type { ContractProcedure, ContractRouter, SchemaInput, SchemaOutput } from '@orpc/contract' import type { Lazy, Procedure, Router } from '@orpc/server' import { createGeneralUtils, type GeneralUtils } from './utils-general' @@ -21,11 +22,11 @@ export type RouterUtils | ContractRouter> = { * @param path - The base path for query key */ export function createRouterUtils | ContractRouter>( - client: any, // TODO typed + client: RouterClient, path: string[] = [], ): RouterUtils { const generalUtils = createGeneralUtils(path) - const procedureUtils = createProcedureUtils(client, path) + const procedureUtils = createProcedureUtils(client as any, path) const recursive = new Proxy({ ...generalUtils, @@ -38,7 +39,7 @@ export function createRouterUtils | ContractRouter>( return value } - const nextUtils = createRouterUtils(client[prop], [...path, prop]) + const nextUtils = createRouterUtils((client as any)[prop], [...path, prop]) if (typeof value !== 'function') { return nextUtils diff --git a/packages/react-query/tests/helpers.tsx b/packages/react-query/tests/helpers.tsx index 6a8b9fe4..e310b8c0 100644 --- a/packages/react-query/tests/helpers.tsx +++ b/packages/react-query/tests/helpers.tsx @@ -106,7 +106,7 @@ export const orpcClient = createORPCClient({ }, }) -export const orpc = createORPCReactQueryUtils(orpcClient) +export const orpc = createORPCReactQueryUtils(orpcClient) export const queryClient = new QueryClient({ defaultOptions: { From 163c7dc8dc20083975ec480e5f5d0927e104d71b Mon Sep 17 00:00:00 2001 From: unnoq Date: Wed, 11 Dec 2024 19:17:16 +0700 Subject: [PATCH 15/17] useQueries tests --- packages/react-query/tests/e2e.test.tsx | 34 ++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/packages/react-query/tests/e2e.test.tsx b/packages/react-query/tests/e2e.test.tsx index f7de841b..044cd1d5 100644 --- a/packages/react-query/tests/e2e.test.tsx +++ b/packages/react-query/tests/e2e.test.tsx @@ -1,4 +1,4 @@ -import { useInfiniteQuery, useMutation, useQuery, useSuspenseInfiniteQuery, useSuspenseQuery } from '@tanstack/react-query' +import { useInfiniteQuery, useMutation, useQueries, useQuery, useSuspenseInfiniteQuery, useSuspenseQuery } from '@tanstack/react-query' import { renderHook } from '@testing-library/react' import { orpc, queryClient } from './helpers' @@ -87,3 +87,35 @@ describe('useSuspenseInfiniteQuery', () => { await vi.waitFor(() => expect(result.current.data?.pages.length).toEqual(2)) }) }) + +describe('useQueries', () => { + it('works - onSuccess', async () => { + const { result } = renderHook(() => useQueries({ + queries: [ + orpc.user.find.queryOptions({ + queryKey: [''], + input: { id: '0' }, + }), + orpc.user.list.queryOptions({ + input: {}, + }), + ], + combine([user, users]) { + return [user, users] as const + }, + }, queryClient)) + + await vi.waitFor(() => expect(queryClient.isFetching({ queryKey: orpc.key() })).toEqual(2)) + await vi.waitFor(() => expect(result.current[0].status).toEqual('success')) + await vi.waitFor(() => expect(result.current[1].status).toEqual('success')) + + expect(result.current[0].data).toEqual({ id: '0', name: 'name-0' }) + expect(result.current[1].data).toEqual({ + users: [ + { id: 'id-0', name: 'number-0' }, + { id: 'id-1', name: 'number-1' }, + ], + nextCursor: 2, + }) + }) +}) From fc8048186a744c89ab19366acff202c9118a5da2 Mon Sep 17 00:00:00 2001 From: unnoq Date: Wed, 11 Dec 2024 19:18:52 +0700 Subject: [PATCH 16/17] docs --- apps/content/content/docs/client/react-query.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/content/content/docs/client/react-query.mdx b/apps/content/content/docs/client/react-query.mdx index c22b7a8a..7aad76bf 100644 --- a/apps/content/content/docs/client/react-query.mdx +++ b/apps/content/content/docs/client/react-query.mdx @@ -24,7 +24,7 @@ export const client = createORPCClient({ }); // Create React Query utilities for ORPC -export const orpc = createORPCReactQueryUtils(client); +export const orpc = createORPCReactQueryUtils(client); // @noErrors orpc.getting. @@ -60,7 +60,7 @@ export function ORPCProvider({ children }: { children: React.ReactNode }) { ); const [queryClient] = React.useState(() => new QueryClient()); - const orpc = React.useMemo(() => createORPCReactQueryUtils(client), [client]); + const orpc = React.useMemo(() => createORPCReactQueryUtils(client), [client]); return ( From 313c241b46e39dec0018cf90cc117e6d13a786ea Mon Sep 17 00:00:00 2001 From: unnoq Date: Wed, 11 Dec 2024 19:21:01 +0700 Subject: [PATCH 17/17] fix --- packages/react-query/tests/e2e.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react-query/tests/e2e.test.tsx b/packages/react-query/tests/e2e.test.tsx index 044cd1d5..53c0d3e5 100644 --- a/packages/react-query/tests/e2e.test.tsx +++ b/packages/react-query/tests/e2e.test.tsx @@ -105,7 +105,6 @@ describe('useQueries', () => { }, }, queryClient)) - await vi.waitFor(() => expect(queryClient.isFetching({ queryKey: orpc.key() })).toEqual(2)) await vi.waitFor(() => expect(result.current[0].status).toEqual('success')) await vi.waitFor(() => expect(result.current[1].status).toEqual('success'))