Skip to content

Commit

Permalink
Merge pull request #131 from wobsoriano/mutations
Browse files Browse the repository at this point in the history
  • Loading branch information
wobsoriano authored Dec 7, 2023
2 parents 7896b95 + 6478abb commit c0d944c
Show file tree
Hide file tree
Showing 10 changed files with 158 additions and 99 deletions.
10 changes: 7 additions & 3 deletions docs/content/1.get-started/5.tips/7.mutation.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,21 @@ The example below shows how you can use Nuxt's [`useNuxtData`](https://nuxt.com/
<script setup lang="ts">
const { $client } = useNuxtApp()
const { data } = await $client.getTodos.useQuery(undefined, { queryKey: 'todos' });
const { data } = await $client.getTodos.useQuery(undefined);
</script>
```

```vue [components/NewTodo.vue]
<script setup lang="ts">
import { getQueryKey } from 'trpc-nuxt/client'
const { $client } = useNuxtApp()
const previousTodos = ref([])
const queryKey = getQueryKey($client.getTodos, undefined)
// Access to the cached value of useQuery in todos.vue
const { data: todos } = useNuxtData('todos')
const { data: todos } = useNuxtData(queryKey)
async function addTodo(payload) {
// Store the previously cached value to restore if mutation fails.
Expand All @@ -32,7 +36,7 @@ async function addTodo(payload) {
try {
await $client.addTodo.mutate(payload)
// Invalidate todos in the background if the mutation succeeded.
await refreshNuxtData('todos')
await refreshNuxtData(queryKey)
} catch {
// Rollback the data if the mutation failed.
todos.value = previousTodos.value
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@
},
"devDependencies": {
"@nuxt/eslint-config": "^0.2.0",
"@trpc/client": "^10.41.0",
"@trpc/server": "^10.41.0",
"@trpc/client": "^10.44.1",
"@trpc/server": "^10.44.1",
"changelogen": "^0.5.5",
"eslint": "^8.52.0",
"taze": "^0.11.4",
Expand Down Expand Up @@ -75,8 +75,8 @@
"pnpm": {
"overrides": {
"nuxt": "3.8.0",
"@trpc/client": "^10.41.0",
"@trpc/server": "^10.41.0"
"@trpc/client": "^10.44.1",
"@trpc/server": "^10.44.1"
}
}
}
4 changes: 2 additions & 2 deletions playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
"postinstall": "nuxi prepare"
},
"dependencies": {
"@trpc/client": "^10.41.0",
"@trpc/server": "^10.41.0",
"@trpc/client": "^10.44.1",
"@trpc/server": "^10.44.1",
"superjson": "^2.1.0",
"trpc-nuxt": "workspace:*",
"zod": "^3.22.4"
Expand Down
27 changes: 16 additions & 11 deletions playground/pages/index.vue
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
<script setup lang="ts">
import { getQueryKey } from 'trpc-nuxt/client'
const { $client } = useNuxtApp()
const todosKey = getQueryKey($client.todo.getTodos, undefined)
const { data } = useNuxtData(todosKey)
const { data: todos, pending, error, refresh } = await $client.todo.getTodos.useQuery()
const addTodo = async () => {
const title = Math.random().toString(36).slice(2, 7)
const newData = {
id: Date.now(),
userId: 69,
title,
completed: false
}
data.value.push(newData)
try {
const x = await $client.todo.addTodo.mutate({
id: Date.now(),
userId: 69,
title,
completed: false
})
console.log(x)
const x = await $client.todo.addTodo.mutate(newData)
} catch (e) {
console.log(e)
}
}
const { data: todos, pending, error, refresh } = await $client.todo.getTodos.useLazyQuery()
</script>

<template>
Expand All @@ -31,7 +36,7 @@ const { data: todos, pending, error, refresh } = await $client.todo.getTodos.use
<div v-else>
<ul>
<li
v-for="t in todos?.slice(0, 10)"
v-for="t in todos"
:key="t.id"
>
<NuxtLink
Expand Down
2 changes: 1 addition & 1 deletion playground/server/trpc/routers/todo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export type Todo = z.infer<typeof TodoShape>
export const todoRouter = router({
getTodos: publicProcedure
.query(() => {
return $fetch<Todo[]>(`${baseURL}/todos`)
return $fetch<Todo[]>(`${baseURL}/todos?_limit=5`)
}),
getTodo: publicProcedure
.input(z.number())
Expand Down
14 changes: 7 additions & 7 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

58 changes: 58 additions & 0 deletions src/client/decorationProxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { type inferRouterProxyClient } from '@trpc/client'
import { type AnyRouter } from '@trpc/server'
import { createRecursiveProxy } from '@trpc/server/shared'
// @ts-expect-error: Nuxt auto-imports
import { getCurrentInstance, onScopeDispose, useAsyncData, unref, isRef } from '#imports'
import { getQueryKeyInternal } from './getQueryKey'

export function createNuxtProxyDecoration<TRouter extends AnyRouter> (name: string, client: inferRouterProxyClient<TRouter>) {
return createRecursiveProxy((opts) => {
const args = opts.args

const pathCopy = [name, ...opts.path]

// The last arg is for instance `.useMutation` or `.useQuery()`
const lastArg = pathCopy.pop()!

// The `path` ends up being something like `post.byId`
const path = pathCopy.join('.')

const [input, otherOptions] = args

if (lastArg === '_def') {
return {
path: pathCopy,
};
}

if (['useQuery', 'useLazyQuery'].includes(lastArg)) {
const { trpc, queryKey: customQueryKey, ...asyncDataOptions } = otherOptions || {} as any

let controller: AbortController

if (trpc?.abortOnUnmount) {
if (getCurrentInstance()) {
onScopeDispose(() => {
controller?.abort?.()
})
}
controller = typeof AbortController !== 'undefined' ? new AbortController() : {} as AbortController
}

const queryKey = customQueryKey || getQueryKeyInternal(path, unref(input))
const watch = isRef(input) ? [...(asyncDataOptions.watch || []), input] : asyncDataOptions.watch
const isLazy = lastArg === 'useLazyQuery' ? true : (asyncDataOptions.lazy || false)

return useAsyncData(queryKey, () => (client as any)[path].query(unref(input), {
signal: controller?.signal,
...trpc
}), {
...asyncDataOptions,
watch,
lazy: isLazy
})
}

return (client as any)[path][lastArg](...args)
})
}
57 changes: 57 additions & 0 deletions src/client/getQueryKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {
AnyQueryProcedure,
AnyRouter,
DeepPartial,
inferProcedureInput,
} from '@trpc/server';
import { hash } from 'ohash'
import { DecorateProcedure } from './types';

export type GetQueryParams<
TProcedureOrRouter extends AnyQueryProcedure,
TProcedureInput = inferProcedureInput<TProcedureOrRouter>,
> = DeepPartial<TProcedureInput>;

type GetParams<
TProcedureOrRouter extends AnyQueryProcedure,
> = [
procedureOrRouter: DecorateProcedure<TProcedureOrRouter, AnyRouter> | string,
params: GetQueryParams<TProcedureOrRouter>,
];

type GetQueryKeyParams<
TProcedureOrRouter extends AnyQueryProcedure,
> = GetParams<TProcedureOrRouter>;

/**
* Method to extract the query key for a procedure
* @param procedure - procedure
* @param input - input to procedure
* @link https://trpc-nuxt.vercel.app/get-started/tips/mutation
*/
export function getQueryKey<
TProcedure extends AnyQueryProcedure,
>(..._params: GetQueryKeyParams<TProcedure>): string {
const [procedure, input] = _params;

if (typeof procedure === 'string') {
// TODO: Warn here if string is passed that it will be deprecated in the future.
return getQueryKeyInternal(procedure, input);
}

// @ts-expect-error: we don't expose _def on the type layer
const path = procedure._def().path as string[];
const dotPath = path.join('.');

return getQueryKeyInternal(dotPath, input)
}

/**
* @internal
*/
export function getQueryKeyInternal (
path: string,
input: unknown
): string {
return input === undefined ? path : `${path}-${hash(input || '')}`
}
75 changes: 5 additions & 70 deletions src/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,76 +1,11 @@
import { type CreateTRPCClientOptions, type inferRouterProxyClient, createTRPCProxyClient } from '@trpc/client'
import { type CreateTRPCClientOptions, createTRPCProxyClient } from '@trpc/client'
import { type AnyRouter } from '@trpc/server'
import { createFlatProxy, createRecursiveProxy } from '@trpc/server/shared'
import { hash } from 'ohash'
import { createFlatProxy } from '@trpc/server/shared'
import { type DecoratedProcedureRecord } from './types'
// @ts-expect-error: Nuxt auto-imports
import { getCurrentInstance, onScopeDispose, useAsyncData, unref, isRef } from '#imports'
import { getQueryKey } from './getQueryKey'
import { createNuxtProxyDecoration } from './decorationProxy'

/**
* Calculates the key used for `useAsyncData` call.
*
* @example
*
* ```ts
* import { getQueryKey } from 'trpc-nuxt/client'
*
* $client.todo.getTodo(1)
*
* const queryKey = getQueryKey('todo.getTodo', 1)
* ```
*/
export function getQueryKey (
path: string,
input: unknown
): string {
return input === undefined ? path : `${path}-${hash(input || '')}`
}

export function createNuxtProxyDecoration<TRouter extends AnyRouter> (name: string, client: inferRouterProxyClient<TRouter>) {
return createRecursiveProxy((opts) => {
const args = opts.args

const pathCopy = [name, ...opts.path]

// The last arg is for instance `.useMutation` or `.useQuery()`
const lastArg = pathCopy.pop()!

// The `path` ends up being something like `post.byId`
const path = pathCopy.join('.')

const [input, otherOptions] = args

if (['useQuery', 'useLazyQuery'].includes(lastArg)) {
const { trpc, queryKey: customQueryKey, ...asyncDataOptions } = otherOptions || {} as any

let controller: AbortController

if (trpc?.abortOnUnmount) {
if (getCurrentInstance()) {
onScopeDispose(() => {
controller?.abort?.()
})
}
controller = typeof AbortController !== 'undefined' ? new AbortController() : {} as AbortController
}

const queryKey = customQueryKey || getQueryKey(path, unref(input))
const watch = isRef(input) ? [...(asyncDataOptions.watch || []), input] : asyncDataOptions.watch
const isLazy = lastArg === 'useLazyQuery' ? true : (asyncDataOptions.lazy || false)

return useAsyncData(queryKey, () => (client as any)[path].query(unref(input), {
signal: controller?.signal,
...trpc
}), {
...asyncDataOptions,
watch,
lazy: isLazy
})
}

return (client as any)[path][lastArg](...args)
})
}
export { getQueryKey }

export function createTRPCNuxtClient<TRouter extends AnyRouter> (opts: CreateTRPCClientOptions<TRouter>) {
const client = createTRPCProxyClient<TRouter>(opts)
Expand Down
2 changes: 1 addition & 1 deletion src/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ type SubscriptionResolver<

type MaybeRef<T> = T | Ref<T>

type DecorateProcedure<
export type DecorateProcedure<
TProcedure extends AnyProcedure,
TRouter extends AnyRouter,
> = TProcedure extends AnyQueryProcedure
Expand Down

1 comment on commit c0d944c

@vercel
Copy link

@vercel vercel bot commented on c0d944c Dec 7, 2023

Choose a reason for hiding this comment

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

Successfully deployed to the following URLs:

trpc-nuxt – ./

trpc-nuxt.vercel.app
trpc-nuxt-wobsoriano.vercel.app
trpc-nuxt-git-main-wobsoriano.vercel.app

Please sign in to comment.