Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(vue-query): minimal integrate with vue query by using queryOptions #47

Merged
merged 17 commits into from
Dec 11, 2024
101 changes: 101 additions & 0 deletions apps/content/content/docs/client/vue-query.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
---
title: Vue Query
description: Simplify Vue Query usage with minimal integration using ORPC and TanStack Query
---

## Installation

```package-install
@orpc/client @orpc/vue-query @tanstack/vue-query
```

## Setup

```ts twoslash
import { createORPCVueQueryUtils } from '@orpc/vue-query';
import { createORPCClient } from '@orpc/client';
import type { router } from 'examples/server';

// Create an ORPC client
export const client = createORPCClient<typeof router>({
baseURL: 'http://localhost:3000/api',
});

// Create Vue Query utilities for ORPC
export const orpc = createORPCVueQueryUtils(client);

// @noErrors
orpc.getting.
// ^|
```

## Multiple ORPC Instances

To prevent conflicts when using multiple ORPC instances, you can provide a unique base path to `createORPCVueQueryUtils`.

```tsx twoslash
import { createORPCVueQueryUtils } from '@orpc/vue-query'

// Create separate ORPC instances with unique base paths
const userORPC = createORPCVueQueryUtils('fake-client' as any, ['__user-api__'])
const postORPC = createORPCVueQueryUtils('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 } from '@tanstack/vue-query'
import { orpc } from 'examples/vue-query'

// Fetch data
const { data: gettingData } = useQuery(orpc.getting.queryOptions({ input: { name: 'unnoq' } }))

// 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.

```ts twoslash
import { os } from '@orpc/server';
import { z } from 'zod';
import { createORPCVueQueryUtils } from '@orpc/vue-query';
import { useInfiniteQuery } from '@tanstack/vue-query';

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 = createORPCVueQueryUtils<typeof router>('fake-client' as any);

const query = useInfiniteQuery(
orpc.user.list.infiniteOptions({
input: { limit: 10 },
getNextPageParam: (lastPage) => lastPage.nextCursor,
initialPageParam: 0,
})
);

console.log(query.data.value);
```
4 changes: 1 addition & 3 deletions apps/content/content/docs/comparisons.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,8 @@ This comparison table helps you understand how oRPC differs from other popular T
| End-to-end Type Safety | ✅ | ✅ | ✅ | Full TypeScript type inference from backend to frontend |
| SSR Support | ✅ | ✅ | ✅ | Server-side rendering compatibility |
| React Query Integration | ✅ | ✅ | 🟡 | Native support for React Query/TanStack Query |
| Vue Query Integration | 🛑 | 🛑 | 🟡 | Native support for Vue Query/TanStack Query |
| Vue Query Integration | | 🛑 | 🟡 | Native support for Vue Query/TanStack Query |
| Contract-First Development | ✅ | 🛑 | ✅ | API definitions before implementation |
| File Operations | ✅ | 🟡 | 🟡 | Built-in support for file uploads/downloads |
| OpenAPI support | ✅ | 🟡 | 🟡 | Generation and consumption of OpenAPI specs |
| Server actions support | ✅ | ✅ | 🛑 | React/Next.js Actions compatibility |

> In the future, we'll implement full Tanstack query support for Vue, Svelte, Solid, ...
6 changes: 2 additions & 4 deletions apps/content/content/home/landing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,7 @@ Our [Vanilla Client](/docs/client/vanilla) is fully typed and doesn't rely on ge

## Seamless Integration with TanStack Query

```tsx
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

```ts
// Fetch data with oRPC
const { data, status } = useQuery(
orpc.post.find.queryOptions({ input: { id: 'example' } })
Expand All @@ -85,7 +83,7 @@ mutate({
});
```

Learn more about [React Query Integration](/docs/client/react-query).
We now support [React Query Integration](/docs/client/react-query) and [Vue Query Integration](/docs/client/vue-query).

## Access via OpenAPI Standard

Expand Down
4 changes: 4 additions & 0 deletions apps/content/examples/vue-query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { router } from 'examples/server'
import { createORPCVueQueryUtils } from '@orpc/vue-query'

export const orpc = createORPCVueQueryUtils<typeof router /** or contract router */>('fake-client' as any)
7 changes: 5 additions & 2 deletions apps/content/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,21 @@
"@orpc/react": "workspace:*",
"@orpc/react-query": "workspace:*",
"@orpc/server": "workspace:*",
"@orpc/vue-query": "workspace:*",
"@orpc/zod": "workspace:*",
"@tanstack/react-query": "^5.62.7",
"@tanstack/vue-query": "^5.62.7",
"@types/express": "^5.0.0",
"@types/mdx": "^2.0.13",
"@types/node": "22.9.0",
"@types/node": "^22.9.0",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@whatwg-node/server": "^0.9.55",
"autoprefixer": "^10.4.20",
"express": "^4.21.1",
"postcss": "^8.4.48",
"tailwindcss": "^3.4.14",
"typescript": "^5.6.3",
"typescript": "5.7.2",
"zod": "^3.23.8"
}
}
1 change: 1 addition & 0 deletions apps/content/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
{ "path": "../../packages/client" },
{ "path": "../../packages/react" },
{ "path": "../../packages/react-query" },
{ "path": "../../packages/vue-query" },
{ "path": "../../packages/zod" },
{ "path": "../../packages/next" }
],
Expand Down
2 changes: 1 addition & 1 deletion packages/next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,6 @@
"@orpc/shared": "workspace:*"
},
"devDependencies": {
"zod": "^3.21.4"
"zod": "^3.23.8"
}
}
2 changes: 1 addition & 1 deletion packages/react-query/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,6 @@
"@orpc/shared": "workspace:*"
},
"devDependencies": {
"zod": "^3.21.4"
"zod": "^3.23.8"
}
}
2 changes: 1 addition & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"@orpc/client": "workspace:*",
"@orpc/contract": "workspace:*",
"@orpc/server": "workspace:*",
"@tanstack/react-query": ">=5.59.0",
"@tanstack/react-query": ">=5.55.0",
"react": ">=18.3.0"
},
"dependencies": {
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/function.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type AnyFunction = (...args: any[]) => any
2 changes: 1 addition & 1 deletion packages/shared/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export * from './function'
export * from './hook'

export * from './json'
export * from './object'
export * from './value'
Expand Down
26 changes: 26 additions & 0 deletions packages/vue-query/.gitignore
Original file line number Diff line number Diff line change
@@ -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
54 changes: 54 additions & 0 deletions packages/vue-query/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"name": "@orpc/vue-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/vue-query"
},
"keywords": [
"unnoq",
"orpc",
"vue",
"vue-query",
"tanstack"
],
"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"
},
"peerDependencies": {
"@orpc/client": "workspace:*",
"@orpc/contract": "workspace:*",
"@orpc/server": "workspace:*",
"@tanstack/vue-query": ">=5.50.0",
"vue": ">=3.3.0"
}
}
9 changes: 9 additions & 0 deletions packages/vue-query/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createRouterUtils } from './utils-router'

export * from './key'
export * from './types'
export * from './utils-general'
export * from './utils-procedure'
export * from './utils-router'

export const createORPCVueQueryUtils = createRouterUtils
17 changes: 17 additions & 0 deletions packages/vue-query/src/key.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { buildKey } from './key'

describe('buildKey', () => {
it('works', () => {
expect(buildKey(['path'])).toEqual(['__ORPC__', ['path'], {}])
expect(buildKey(['path'], { type: 'query' })).toEqual(['__ORPC__', ['path'], { type: 'query' }])

expect(buildKey(['path'], { type: 'query', input: { a: 1 } }))
.toEqual(['__ORPC__', ['path'], { type: 'query', input: { a: 1 } }])

expect(buildKey(['path'], { type: 'query', input: undefined }))
.toEqual(['__ORPC__', ['path'], { type: 'query' }])

expect(buildKey(['path'], { type: undefined, input: undefined }))
.toEqual(['__ORPC__', ['path'], {}])
})
})
29 changes: 29 additions & 0 deletions packages/vue-query/src/key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { PartialDeep } from '@orpc/shared'
import type { QueryKey } from '@tanstack/vue-query'

// TODO: this file duplicate with react query

export type KeyType = 'query' | 'infinite' | 'mutation' | undefined

export interface BuildKeyOptions<TType extends KeyType, TInput> {
type?: TType
input?: TType extends 'mutation' ? never : PartialDeep<TInput>
}

export function buildKey<TType extends KeyType, TInput>(
path: string[],
options?: BuildKeyOptions<TType, TInput>,
): QueryKey {
const withInput
= options?.input !== undefined ? { input: options?.input } : {}
const withType = options?.type !== undefined ? { type: options?.type } : {}

return [
'__ORPC__',
path,
{
...withInput,
...withType,
},
]
}
44 changes: 44 additions & 0 deletions packages/vue-query/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { AnyFunction, SetOptional } from '@orpc/shared'
import type { DefaultError, MutationObserverOptions, QueryKey, QueryObserverOptions, UseInfiniteQueryOptions } from '@tanstack/vue-query'
import type { MaybeRef, MaybeRefOrGetter } from 'vue'

export type MaybeDeepRef<T> = MaybeRef<
T extends AnyFunction
? T
: T extends object
? {
[K in keyof T]: MaybeDeepRef<T[K]>
}
: T
>

export type NonUndefinedGuard<T> = T extends undefined ? never : T

export type InferCursor<T> = T extends { cursor?: any } ? T['cursor'] : never

export type QueryOptions<TInput, TOutput, TSelectData> =
& (undefined extends TInput ? { input?: MaybeDeepRef<TInput> } : { input: MaybeDeepRef<TInput> })
& SetOptional<{
[P in keyof QueryObserverOptions<TOutput, DefaultError, TSelectData, TOutput, QueryKey>]: P extends 'enabled'
? MaybeRefOrGetter<MaybeDeepRef<QueryObserverOptions<TOutput, DefaultError, TSelectData, TOutput, QueryKey>[P]>>
: MaybeDeepRef<QueryObserverOptions<TOutput, DefaultError, TSelectData, TOutput, QueryKey>[P]>
}, 'queryKey'>
& {
shallow?: MaybeRef<boolean>
initialData?: NonUndefinedGuard<TOutput> | (() => NonUndefinedGuard<TOutput>) | undefined
}

export type InfiniteOptions<TInput, TOutput, TSelectData> =
& (undefined extends TInput ? { input?: MaybeDeepRef<Omit<TInput, 'cursor'>> } : { input: MaybeDeepRef<Omit<TInput, 'cursor'>> })
& SetOptional<
UseInfiniteQueryOptions<TOutput, DefaultError, TSelectData, TOutput, QueryKey, InferCursor<TInput>>,
'queryKey' | (undefined extends InferCursor<TInput> ? 'initialPageParam' : never)
>

export type MutationOptions<TInput, TOutput> =
& {
[P in keyof MutationObserverOptions<TOutput, DefaultError, TInput, unknown>]: MaybeDeepRef<MutationObserverOptions<TOutput, DefaultError, TInput, unknown>[P]>
}
& {
shallow?: MaybeRef<boolean>
}
Loading
Loading