Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/many-symbols-write.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/vue-query': minor
---

feat(vue-query): add 'mutationOptions'
4 changes: 4 additions & 0 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -1067,6 +1067,10 @@
"label": "infiniteQueryOptions",
"to": "framework/vue/reference/infiniteQueryOptions"
},
{
"label": "mutationOptions",
"to": "framework/vue/reference/mutationOptions"
},
{
"label": "usePrefetchQuery",
"to": "framework/vue/reference/usePrefetchQuery"
Expand Down
5 changes: 5 additions & 0 deletions docs/framework/vue/reference/mutationOptions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
id: mutationOptions
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You also need to add reference in the config
Ex.
https://github.com/TanStack/query/blob/main/docs/config.json#L1003

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@DamianOsipiuk Added mutationOptions entry to docs/config.json. (0761b46)

title: mutationOptions
ref: docs/framework/react/reference/mutationOptions.md
---
246 changes: 246 additions & 0 deletions packages/vue-query/src/__tests__/mutationOptions.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
import { assertType, describe, expectTypeOf, it } from 'vitest'
import { QueryClient } from '@tanstack/query-core'
import { useMutation } from '../useMutation'
import { useIsMutating, useMutationState } from '../useMutationState'
import { mutationOptions } from '../mutationOptions'
import type {
DefaultError,
MutationFunctionContext,
MutationState,
WithRequired,
} from '@tanstack/query-core'
import type { Ref } from 'vue-demi'
import type { MutationOptions } from '../types'

describe('mutationOptions', () => {
it('should not allow excess properties', () => {
mutationOptions({
// @ts-expect-error this is a good error, because onMutates does not exist!
mutationFn: () => Promise.resolve(5),
mutationKey: ['key'],
onMutates: 1000,
Comment on lines +18 to +21
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read-only verification: confirm which line each `@ts-expect-error` currently guards.
file="packages/vue-query/src/__tests__/mutationOptions.test-d.ts"

awk '
/@ts-expect-error/{
  dirLine=NR
  while (getline next) {
    if (next ~ /^[[:space:]]*$/) continue
    print "Directive at line " dirLine
    print "Next non-empty line: " NR ": " next
    print "---"
    break
  }
}
' "$file"

Repository: TanStack/query

Length of output: 150


🏁 Script executed:

cat -n packages/vue-query/src/__tests__/mutationOptions.test-d.ts | sed -n '16,25p'

Repository: TanStack/query

Length of output: 473


🏁 Script executed:

rg "@ts-expect-error" packages/vue-query/src/__tests__/mutationOptions.test-d.ts -n

Repository: TanStack/query

Length of output: 423


🏁 Script executed:

cat -n packages/vue-query/src/__tests__/mutationOptions.test-d.ts | sed -n '95,105p'

Repository: TanStack/query

Length of output: 517


🏁 Script executed:

cat -n packages/vue-query/src/__tests__/mutationOptions.test-d.ts | sed -n '173,180p'

Repository: TanStack/query

Length of output: 335


🏁 Script executed:

cat -n packages/vue-query/src/__tests__/mutationOptions.test-d.ts | sed -n '192,200p'

Repository: TanStack/query

Length of output: 351


🏁 Script executed:

cat -n packages/vue-query/src/__tests__/mutationOptions.test-d.ts | sed -n '211,220p'

Repository: TanStack/query

Length of output: 319


@ts-expect-error directive is on the wrong line.

The directive on line 18 applies to line 19 (mutationFn), but the intentional error is on line 21 (onMutates). Move the directive to line 21 to guard the correct property.

Fix
     mutationOptions({
-      // `@ts-expect-error` this is a good error, because onMutates does not exist!
       mutationFn: () => Promise.resolve(5),
       mutationKey: ['key'],
+      // `@ts-expect-error` this is a good error, because onMutates does not exist!
       onMutates: 1000,
       onSuccess: (data) => {
         expectTypeOf(data).toEqualTypeOf<number>()
       },
     })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// @ts-expect-error this is a good error, because onMutates does not exist!
mutationFn: () => Promise.resolve(5),
mutationKey: ['key'],
onMutates: 1000,
mutationFn: () => Promise.resolve(5),
mutationKey: ['key'],
// `@ts-expect-error` this is a good error, because onMutates does not exist!
onMutates: 1000,
onSuccess: (data) => {
expectTypeOf(data).toEqualTypeOf<number>()
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vue-query/src/__tests__/mutationOptions.test-d.ts` around lines 18 -
21, The `@ts-expect-error` is placed before mutationFn but the intentional
TypeScript error is on the onMutates property; move the `@ts-expect-error`
directive so it precedes the onMutates line (the line containing onMutates:
1000) to correctly suppress the type error for onMutates while leaving
mutationFn and mutationKey unchanged.

onSuccess: (data) => {
expectTypeOf(data).toEqualTypeOf<number>()
},
})
})

it('should infer types for callbacks', () => {
mutationOptions({
mutationFn: () => Promise.resolve(5),
mutationKey: ['key'],
onSuccess: (data) => {
expectTypeOf(data).toEqualTypeOf<number>()
},
})
})

it('should infer types for onError callback', () => {
mutationOptions({
mutationFn: () => {
throw new Error('fail')
},
mutationKey: ['key'],
onError: (error) => {
expectTypeOf(error).toEqualTypeOf<DefaultError>()
},
})
})

it('should infer types for variables', () => {
mutationOptions<number, DefaultError, { id: string }>({
mutationFn: (vars) => {
expectTypeOf(vars).toEqualTypeOf<{ id: string }>()
return Promise.resolve(5)
},
mutationKey: ['with-vars'],
})
})

it('should infer result type correctly', () => {
mutationOptions<number, DefaultError, void, { name: string }>({
mutationFn: () => Promise.resolve(5),
mutationKey: ['key'],
onMutate: () => {
return { name: 'onMutateResult' }
},
onSuccess: (_data, _variables, onMutateResult) => {
expectTypeOf(onMutateResult).toEqualTypeOf<{ name: string }>()
},
})
})

it('should infer context type correctly', () => {
mutationOptions<number>({
mutationFn: (_variables, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
return Promise.resolve(5)
},
mutationKey: ['key'],
onMutate: (_variables, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
},
onSuccess: (_data, _variables, _onMutateResult, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
},
onError: (_error, _variables, _onMutateResult, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
},
onSettled: (_data, _error, _variables, _onMutateResult, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
},
})
})

it('should error if mutationFn return type mismatches TData', () => {
assertType(
mutationOptions<number>({
// @ts-expect-error this is a good error, because return type is string, not number
mutationFn: async () => Promise.resolve('wrong return'),
}),
)
})

it('should allow mutationKey to be omitted', () => {
return mutationOptions({
mutationFn: () => Promise.resolve(123),
onSuccess: (data) => {
expectTypeOf(data).toEqualTypeOf<number>()
},
})
})

it('should infer all types when not explicitly provided', () => {
expectTypeOf(
mutationOptions({
mutationFn: (id: string) => Promise.resolve(id.length),
mutationKey: ['key'],
onSuccess: (data) => {
expectTypeOf(data).toEqualTypeOf<number>()
},
}),
).toEqualTypeOf<
WithRequired<
MutationOptions<number, DefaultError, string, unknown>,
'mutationKey'
>
>()
expectTypeOf(
mutationOptions({
mutationFn: (id: string) => Promise.resolve(id.length),
onSuccess: (data) => {
expectTypeOf(data).toEqualTypeOf<number>()
},
}),
).toEqualTypeOf<
Omit<
MutationOptions<number, DefaultError, string, unknown>,
'mutationKey'
>
>()
})

it('should work when used with useMutation', () => {
const mutation = useMutation(
mutationOptions({
mutationKey: ['key'],
mutationFn: () => Promise.resolve('data'),
onSuccess: (data) => {
expectTypeOf(data).toEqualTypeOf<string>()
},
}),
)
expectTypeOf(mutation.data.value).toEqualTypeOf<string | undefined>()

// should allow when used with useMutation without mutationKey
useMutation(
mutationOptions({
mutationFn: () => Promise.resolve('data'),
onSuccess: (data) => {
expectTypeOf(data).toEqualTypeOf<string>()
},
}),
)
})

it('should work when used with useIsMutating', () => {
const isMutating = useIsMutating(
mutationOptions({
mutationKey: ['key'],
mutationFn: () => Promise.resolve(5),
}),
)
expectTypeOf(isMutating).toEqualTypeOf<Ref<number>>()

useIsMutating(
// @ts-expect-error filters should have mutationKey
mutationOptions({
mutationFn: () => Promise.resolve(5),
}),
)
})

it('should work when used with queryClient.isMutating', () => {
const queryClient = new QueryClient()

const isMutating = queryClient.isMutating(
mutationOptions({
mutationKey: ['key'],
mutationFn: () => Promise.resolve(5),
}),
)
expectTypeOf(isMutating).toEqualTypeOf<number>()

queryClient.isMutating(
// @ts-expect-error filters should have mutationKey
mutationOptions({
mutationFn: () => Promise.resolve(5),
}),
)
})

it('should work when used with useMutationState', () => {
const mutationState = useMutationState({
filters: mutationOptions({
mutationKey: ['key'],
mutationFn: () => Promise.resolve(5),
}),
})
expectTypeOf(mutationState.value).toEqualTypeOf<
Array<MutationState<unknown, Error, unknown, unknown>>
>()

useMutationState({
// @ts-expect-error filters should have mutationKey
filters: mutationOptions({
mutationFn: () => Promise.resolve(5),
}),
})
})

it('should allow getter and infer types correctly', () => {
const options = mutationOptions(() => ({
mutationKey: ['key'] as const,
mutationFn: () => Promise.resolve('data'),
onSuccess: (data) => {
expectTypeOf(data).toEqualTypeOf<string>()
},
}))

const resolved = options()
expectTypeOf(resolved.mutationFn).not.toBeUndefined()
expectTypeOf(resolved.mutationKey).not.toBeUndefined()
})

it('should allow getter without mutationKey', () => {
const options = mutationOptions(() => ({
mutationFn: () => Promise.resolve(5),
onSuccess: (data) => {
expectTypeOf(data).toEqualTypeOf<number>()
},
}))

const resolved = options()
expectTypeOf(resolved.mutationFn).not.toBeUndefined()
})
})
Loading
Loading