From 520cdf4d6ea1dbd2bbc2d1cec02faac007361df7 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sun, 3 May 2026 17:22:05 -0700 Subject: [PATCH] refactor(tanstack-query): strongly type TransactionOperation and normalize model name lookup - Make `TransactionOperation` a discriminated union over (model, op) pairs with schema-derived args types - Normalize incoming mutation model name via case-insensitive schema lookup so lowerCaseFirst names resolve - Guard transaction onSuccess against malformed variables and per-item shape - Move `normalizeEndpoint` from constants.ts to client.ts - Group react-query tests into CRUD/Optimistic/Sequential describe blocks Co-Authored-By: Claude Sonnet 4.6 --- .../client-helpers/src/invalidation.ts | 9 +- .../src/nested-write-visitor.ts | 4 - .../tanstack-query/src/common/transaction.ts | 9 +- .../tanstack-query/src/common/types.ts | 64 +- packages/clients/tanstack-query/src/react.ts | 15 +- .../tanstack-query/src/svelte/index.svelte.ts | 15 +- packages/clients/tanstack-query/src/vue.ts | 19 +- .../tanstack-query/test/react-query.test.tsx | 3298 +++++++++-------- 8 files changed, 1752 insertions(+), 1681 deletions(-) diff --git a/packages/clients/client-helpers/src/invalidation.ts b/packages/clients/client-helpers/src/invalidation.ts index 1289a881..ef792fe2 100644 --- a/packages/clients/client-helpers/src/invalidation.ts +++ b/packages/clients/client-helpers/src/invalidation.ts @@ -30,10 +30,11 @@ export function createInvalidator( invalidator: InvalidateFunc, logging: Logger | undefined, ) { + const normalizedModel = normalizeModelName(model, schema); return async (...args: unknown[]) => { const [_, variables] = args; const predicate = await getInvalidationPredicate( - model, + normalizedModel, operation as ORMWriteActionType, variables, schema, @@ -87,3 +88,9 @@ function findNestedRead(visitingModel: string, targetModels: string[], schema: S const modelsRead = getReadModels(visitingModel, schema, args); return targetModels.some((m) => modelsRead.includes(m)); } + +// resolves a model name to its canonical form as defined in the schema (case-insensitive match) +function normalizeModelName(model: string, schema: SchemaDef) { + const target = model.toLowerCase(); + return Object.keys(schema.models).find((k) => k.toLowerCase() === target) ?? model; +} diff --git a/packages/clients/client-helpers/src/nested-write-visitor.ts b/packages/clients/client-helpers/src/nested-write-visitor.ts index 14ca1e40..f4ec614b 100644 --- a/packages/clients/client-helpers/src/nested-write-visitor.ts +++ b/packages/clients/client-helpers/src/nested-write-visitor.ts @@ -297,10 +297,6 @@ export class NestedWriteVisitor { } } break; - - default: { - throw new Error(`unhandled action type ${action}`); - } } } diff --git a/packages/clients/tanstack-query/src/common/transaction.ts b/packages/clients/tanstack-query/src/common/transaction.ts index 6f31a8a3..f4cff0fa 100644 --- a/packages/clients/tanstack-query/src/common/transaction.ts +++ b/packages/clients/tanstack-query/src/common/transaction.ts @@ -9,8 +9,11 @@ import type { TransactionOperation } from './types.js'; /** * Builds the mutation function for a sequential transaction request. */ -export function makeTransactionMutationFn(endpoint: string, fetch: FetchFn | undefined) { - return (operations: TransactionOperation[]) => { +export function makeTransactionMutationFn( + endpoint: string, + fetch: FetchFn | undefined, +) { + return (operations: TransactionOperation[]) => { const reqUrl = `${endpoint}/${TRANSACTION_ROUTE_PREFIX}/sequential`; const fetchInit = { method: 'POST', @@ -37,7 +40,7 @@ export function makeTransactionOnSuccess( origOnSuccess: ((...args: any[]) => any) | undefined, ) { return async (...args: any[]) => { - const variables = Array.isArray(args[1]) ? (args[1] as TransactionOperation[]) : []; + const variables = Array.isArray(args[1]) ? args[1] : []; for (const op of variables) { if (typeof op?.model !== 'string' || typeof op?.op !== 'string') { continue; diff --git a/packages/clients/tanstack-query/src/common/types.ts b/packages/clients/tanstack-query/src/common/types.ts index 262f7761..016adcd9 100644 --- a/packages/clients/tanstack-query/src/common/types.ts +++ b/packages/clients/tanstack-query/src/common/types.ts @@ -1,12 +1,28 @@ import type { Logger, OptimisticDataProvider } from '@zenstackhq/client-helpers'; import type { FetchFn } from '@zenstackhq/client-helpers/fetch'; import type { + AggregateArgs, + CountArgs, + CreateArgs, + CreateManyAndReturnArgs, + CreateManyArgs, + DeleteArgs, + DeleteManyArgs, + ExistsArgs, + FindFirstArgs, + FindManyArgs, + FindUniqueArgs, GetProcedureNames, GetSlicedOperations, + GroupByArgs, ModelAllowsCreate, OperationsRequiringCreate, ProcedureFunc, QueryOptions, + UpdateArgs, + UpdateManyAndReturnArgs, + UpdateManyArgs, + UpsertArgs, } from '@zenstackhq/orm'; import type { GetModels, SchemaDef } from '@zenstackhq/schema'; @@ -102,10 +118,48 @@ export type ProcedureReturn; /** - * Represents a single operation to execute within a sequential transaction. + * Maps each core CRUD operation to its argument type for a given model. */ -export type TransactionOperation = { - model: string; - op: string; - args?: unknown; +type CrudArgsMap> = { + findMany: FindManyArgs; + findUnique: FindUniqueArgs; + findFirst: FindFirstArgs; + create: CreateArgs; + createMany: CreateManyArgs; + createManyAndReturn: CreateManyAndReturnArgs; + update: UpdateArgs; + updateMany: UpdateManyArgs; + updateManyAndReturn: UpdateManyAndReturnArgs; + upsert: UpsertArgs; + delete: DeleteArgs; + deleteMany: DeleteManyArgs; + count: CountArgs; + aggregate: AggregateArgs; + groupBy: GroupByArgs; + exists: ExistsArgs; }; + +/** + * Operations available for a given model, omitting create-style operations + * for models that don't allow them (e.g. delegate models). + */ +type AllowedTransactionOps> = + ModelAllowsCreate extends true + ? keyof CrudArgsMap + : Exclude, OperationsRequiringCreate>; + +/** + * Represents a single operation to execute within a sequential transaction. + * + * The `model`, `op`, and `args` fields are correlated: `op` is constrained to + * the CRUD operations available on `model`, and `args` is typed accordingly. + */ +export type TransactionOperation = { + [Model in GetModels]: { + [Op in AllowedTransactionOps]: { + model: Model; + op: Op; + args?: CrudArgsMap[Op]; + }; + }[AllowedTransactionOps]; +}[GetModels]; diff --git a/packages/clients/tanstack-query/src/react.ts b/packages/clients/tanstack-query/src/react.ts index e087cff1..731f3fda 100644 --- a/packages/clients/tanstack-query/src/react.ts +++ b/packages/clients/tanstack-query/src/react.ts @@ -167,8 +167,8 @@ export type ModelMutationModelResult< ): Promise>; }; -export type TransactionMutationOptions = Omit< - UseMutationOptions, +export type TransactionMutationOptions = Omit< + UseMutationOptions[]>, 'mutationFn' > & Omit; @@ -187,8 +187,8 @@ export type ClientHooks< } & ProcedureHooks & { $transaction: { useSequential( - options?: TransactionMutationOptions, - ): UseMutationResult; + options?: TransactionMutationOptions, + ): UseMutationResult[]>; }; }; @@ -807,11 +807,14 @@ export function useInternalMutation( return useMutation(finalOptions); } -export function useInternalTransactionMutation(schema: SchemaDef, options?: TransactionMutationOptions) { +export function useInternalTransactionMutation( + schema: Schema, + options?: TransactionMutationOptions, +) { const { endpoint, fetch, logging } = useFetchOptions(options); const queryClient = useQueryClient(); - const mutationFn = makeTransactionMutationFn(endpoint, fetch); + const mutationFn = makeTransactionMutationFn(endpoint, fetch); const finalOptions = { ...options, mutationFn }; diff --git a/packages/clients/tanstack-query/src/svelte/index.svelte.ts b/packages/clients/tanstack-query/src/svelte/index.svelte.ts index 85122515..b8bf2c9b 100644 --- a/packages/clients/tanstack-query/src/svelte/index.svelte.ts +++ b/packages/clients/tanstack-query/src/svelte/index.svelte.ts @@ -160,8 +160,8 @@ export type ModelMutationModelResult< ): Promise>; }; -export type TransactionMutationOptions = Omit< - CreateMutationOptions, +export type TransactionMutationOptions = Omit< + CreateMutationOptions[]>, 'mutationFn' > & Omit; @@ -180,8 +180,8 @@ export type ClientHooks< } & ProcedureHooks & { $transaction: { useSequential( - options?: TransactionMutationOptions, - ): CreateMutationResult; + options?: TransactionMutationOptions, + ): CreateMutationResult[]>; }; }; @@ -709,11 +709,14 @@ export function useInternalMutation( return createMutation(finalOptions); } -export function useInternalTransactionMutation(schema: SchemaDef, options?: Accessor) { +export function useInternalTransactionMutation( + schema: Schema, + options?: Accessor>, +) { const { endpoint, fetch, logging } = useFetchOptions(options); const queryClient = useQueryClient(); - const mutationFn = makeTransactionMutationFn(endpoint, fetch); + const mutationFn = makeTransactionMutationFn(endpoint, fetch); const finalOptions = () => { const optionsValue = options?.(); diff --git a/packages/clients/tanstack-query/src/vue.ts b/packages/clients/tanstack-query/src/vue.ts index c0f0ccad..76669b1a 100644 --- a/packages/clients/tanstack-query/src/vue.ts +++ b/packages/clients/tanstack-query/src/vue.ts @@ -154,8 +154,8 @@ export type ModelMutationModelResult< ): Promise>; }; -export type TransactionMutationOptions = MaybeRefOrGetter< - Omit>, 'mutationFn'> & +export type TransactionMutationOptions = MaybeRefOrGetter< + Omit[]>>, 'mutationFn'> & Omit >; @@ -173,8 +173,8 @@ export type ClientHooks< } & ProcedureHooks & { $transaction: { useSequential( - options?: TransactionMutationOptions, - ): UseMutationReturnType; + options?: TransactionMutationOptions, + ): UseMutationReturnType[], unknown>; }; }; @@ -726,17 +726,14 @@ export function useInternalMutation( return useMutation(finalOptions); } -export function useInternalTransactionMutation( - schema: SchemaDef, - options?: MaybeRefOrGetter< - Omit>, 'mutationFn'> & - Omit - >, +export function useInternalTransactionMutation( + schema: Schema, + options?: TransactionMutationOptions, ) { const queryClient = useQueryClient(); const { endpoint, fetch, logging } = useFetchOptions(options); - const mutationFn = makeTransactionMutationFn(endpoint, fetch); + const mutationFn = makeTransactionMutationFn(endpoint, fetch); const finalOptions = computed(() => { const optionsValue = toValue(options); diff --git a/packages/clients/tanstack-query/test/react-query.test.tsx b/packages/clients/tanstack-query/test/react-query.test.tsx index 37bd462b..6afae03d 100644 --- a/packages/clients/tanstack-query/test/react-query.test.tsx +++ b/packages/clients/tanstack-query/test/react-query.test.tsx @@ -45,1833 +45,1841 @@ describe('React Query Test', () => { cleanup(); }); - it('works with simple query', async () => { - const { queryClient, wrapper } = createWrapper(); + describe('CRUD and invalidation', () => { + it('works with simple query', async () => { + const { queryClient, wrapper } = createWrapper(); - const queryArgs = { where: { id: '1' } }; - const data = { id: '1', name: 'foo' }; + const queryArgs = { where: { id: '1' } }; + const data = { id: '1', name: 'foo' }; - nock(makeUrl('User', 'findUnique', queryArgs)) - .get(/.*/) - .reply(200, { - data, - }); + nock(makeUrl('User', 'findUnique', queryArgs)) + .get(/.*/) + .reply(200, { + data, + }); - const { result } = renderHook(() => useClientQueries(schema).user.useFindUnique(queryArgs), { - wrapper, - }); - - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - expect(result.current.data).toMatchObject(data); - const cacheData = queryClient.getQueryData(getQueryKey('User', 'findUnique', queryArgs)); - expect(cacheData).toMatchObject(data); - }); - - nock(makeUrl('User', 'findFirst', queryArgs)) - .get(/.*/) - .reply(404, () => { - return { error: 'Not Found' }; - }); - const { result: errorResult } = renderHook(() => useClientQueries(schema).user.useFindFirst(queryArgs), { - wrapper, - }); - await waitFor(() => { - expect(errorResult.current.isError).toBe(true); - }); - }); - - it('works with suspense query', async () => { - const { queryClient, wrapper } = createWrapper(); - - const queryArgs = { where: { id: '1' } }; - const data = { id: '1', name: 'foo' }; - - nock(makeUrl('User', 'findUnique', queryArgs)) - .get(/.*/) - .reply(200, { - data, - }); - - const { result } = renderHook(() => useClientQueries(schema).user.useSuspenseFindUnique(queryArgs), { - wrapper, - }); - - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - expect(result.current.data).toMatchObject(data); - const cacheData = queryClient.getQueryData(getQueryKey('User', 'findUnique', queryArgs)); - expect(cacheData).toMatchObject(data); - }); - }); - - it('works with infinite query', async () => { - const { queryClient, wrapper } = createWrapper(); - - const queryArgs = { where: { id: '1' } }; - const data = [{ id: '1', name: 'foo' }]; - - nock(makeUrl('User', 'findMany', queryArgs)) - .get(/.*/) - .reply(200, () => ({ - data, - })); - - const { result } = renderHook( - () => - useClientQueries(schema).user.useInfiniteFindMany(queryArgs, { - getNextPageParam: () => null, - }), - { + const { result } = renderHook(() => useClientQueries(schema).user.useFindUnique(queryArgs), { wrapper, - }, - ); - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - const resultData = result.current.data!; - expect(resultData.pages).toHaveLength(1); - expect(resultData.pages[0]).toMatchObject(data); - expect(resultData?.pageParams).toHaveLength(1); - expect(resultData?.pageParams[0]).toMatchObject(queryArgs); - expect(result.current.hasNextPage).toBe(false); - const cacheData: any = queryClient.getQueryData( - getQueryKey('User', 'findMany', queryArgs, { infinite: true, optimisticUpdate: false }), + }); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + expect(result.current.data).toMatchObject(data); + const cacheData = queryClient.getQueryData(getQueryKey('User', 'findUnique', queryArgs)); + expect(cacheData).toMatchObject(data); + }); + + nock(makeUrl('User', 'findFirst', queryArgs)) + .get(/.*/) + .reply(404, () => { + return { error: 'Not Found' }; + }); + const { result: errorResult } = renderHook(() => useClientQueries(schema).user.useFindFirst(queryArgs), { + wrapper, + }); + await waitFor(() => { + expect(errorResult.current.isError).toBe(true); + }); + }); + + it('works with suspense query', async () => { + const { queryClient, wrapper } = createWrapper(); + + const queryArgs = { where: { id: '1' } }; + const data = { id: '1', name: 'foo' }; + + nock(makeUrl('User', 'findUnique', queryArgs)) + .get(/.*/) + .reply(200, { + data, + }); + + const { result } = renderHook(() => useClientQueries(schema).user.useSuspenseFindUnique(queryArgs), { + wrapper, + }); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + expect(result.current.data).toMatchObject(data); + const cacheData = queryClient.getQueryData(getQueryKey('User', 'findUnique', queryArgs)); + expect(cacheData).toMatchObject(data); + }); + }); + + it('works with infinite query', async () => { + const { queryClient, wrapper } = createWrapper(); + + const queryArgs = { where: { id: '1' } }; + const data = [{ id: '1', name: 'foo' }]; + + nock(makeUrl('User', 'findMany', queryArgs)) + .get(/.*/) + .reply(200, () => ({ + data, + })); + + const { result } = renderHook( + () => + useClientQueries(schema).user.useInfiniteFindMany(queryArgs, { + getNextPageParam: () => null, + }), + { + wrapper, + }, ); - expect(cacheData.pages[0]).toMatchObject(data); + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + const resultData = result.current.data!; + expect(resultData.pages).toHaveLength(1); + expect(resultData.pages[0]).toMatchObject(data); + expect(resultData?.pageParams).toHaveLength(1); + expect(resultData?.pageParams[0]).toMatchObject(queryArgs); + expect(result.current.hasNextPage).toBe(false); + const cacheData: any = queryClient.getQueryData( + getQueryKey('User', 'findMany', queryArgs, { infinite: true, optimisticUpdate: false }), + ); + expect(cacheData.pages[0]).toMatchObject(data); + }); }); - }); - it('works with suspense infinite query', async () => { - const { queryClient, wrapper } = createWrapper(); + it('works with suspense infinite query', async () => { + const { queryClient, wrapper } = createWrapper(); - const queryArgs = { where: { id: '1' } }; - const data = [{ id: '1', name: 'foo' }]; + const queryArgs = { where: { id: '1' } }; + const data = [{ id: '1', name: 'foo' }]; - nock(makeUrl('User', 'findMany', queryArgs)) - .get(/.*/) - .reply(200, () => ({ - data, - })); + nock(makeUrl('User', 'findMany', queryArgs)) + .get(/.*/) + .reply(200, () => ({ + data, + })); - const { result } = renderHook( - () => - useClientQueries(schema).user.useSuspenseInfiniteFindMany(queryArgs, { - getNextPageParam: () => null, - }), - { - wrapper, - }, - ); - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - const resultData = result.current.data!; - expect(resultData.pages).toHaveLength(1); - expect(resultData.pages[0]).toMatchObject(data); - expect(resultData?.pageParams).toHaveLength(1); - expect(resultData?.pageParams[0]).toMatchObject(queryArgs); - expect(result.current.hasNextPage).toBe(false); - const cacheData: any = queryClient.getQueryData( - getQueryKey('User', 'findMany', queryArgs, { infinite: true, optimisticUpdate: false }), + const { result } = renderHook( + () => + useClientQueries(schema).user.useSuspenseInfiniteFindMany(queryArgs, { + getNextPageParam: () => null, + }), + { + wrapper, + }, ); - expect(cacheData.pages[0]).toMatchObject(data); - }); - }); - - it('works with independent mutation and query', async () => { - const { wrapper } = createWrapper(); - - const queryArgs = { where: { id: '1' } }; - const data = { id: '1', name: 'foo' }; - - let queryCount = 0; - nock(makeUrl('User', 'findUnique', queryArgs)) - .get(/.*/) - .reply(200, () => { - queryCount++; - return { data }; - }) - .persist(); - - const { result } = renderHook(() => useClientQueries(schema).user.useFindUnique(queryArgs), { - wrapper, - }); - await waitFor(() => { - expect(result.current.data).toMatchObject({ name: 'foo' }); + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + const resultData = result.current.data!; + expect(resultData.pages).toHaveLength(1); + expect(resultData.pages[0]).toMatchObject(data); + expect(resultData?.pageParams).toHaveLength(1); + expect(resultData?.pageParams[0]).toMatchObject(queryArgs); + expect(result.current.hasNextPage).toBe(false); + const cacheData: any = queryClient.getQueryData( + getQueryKey('User', 'findMany', queryArgs, { infinite: true, optimisticUpdate: false }), + ); + expect(cacheData.pages[0]).toMatchObject(data); + }); }); - nock(makeUrl('Post', 'create')) - .post(/.*/) - .reply(200, () => ({ - data: { id: '1', title: 'post1' }, - })); + it('works with independent mutation and query', async () => { + const { wrapper } = createWrapper(); - const { result: mutationResult } = renderHook(() => useClientQueries(schema).post.useCreate(), { - wrapper, - }); + const queryArgs = { where: { id: '1' } }; + const data = { id: '1', name: 'foo' }; - act(() => mutationResult.current.mutate({ data: { title: 'post1' } })); + let queryCount = 0; + nock(makeUrl('User', 'findUnique', queryArgs)) + .get(/.*/) + .reply(200, () => { + queryCount++; + return { data }; + }) + .persist(); - await waitFor(() => { - // no refetch caused by invalidation - expect(queryCount).toBe(1); - }); - }); - - it('works with create and invalidation', async () => { - const { queryClient, wrapper } = createWrapper(); - - const data: any[] = []; - - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => ({ data })) - .persist(); - - const { result } = renderHook(() => useClientQueries(schema).user.useFindMany(), { - wrapper, - }); - await waitFor(() => { - expect(result.current.data).toHaveLength(0); - }); - - nock(makeUrl('User', 'create')) - .post(/.*/) - .reply(200, () => { - data.push({ id: '1', email: 'foo' }); - return { data: data[0] }; + const { result } = renderHook(() => useClientQueries(schema).user.useFindUnique(queryArgs), { + wrapper, + }); + await waitFor(() => { + expect(result.current.data).toMatchObject({ name: 'foo' }); }); - const { result: mutationResult } = renderHook(() => useClientQueries(schema).user.useCreate(), { - wrapper, - }); + nock(makeUrl('Post', 'create')) + .post(/.*/) + .reply(200, () => ({ + data: { id: '1', title: 'post1' }, + })); - act(() => mutationResult.current.mutate({ data: { email: 'foo' } })); - - await waitFor(() => { - const cacheData = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined)); - expect(cacheData).toHaveLength(1); - }); - }); - - it('works with create and no invalidation', async () => { - const { queryClient, wrapper } = createWrapper(); - - const data: any[] = []; - - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => ({ data })) - .persist(); - - const { result } = renderHook(() => useClientQueries(schema).user.useFindMany(), { - wrapper, - }); - await waitFor(() => { - expect(result.current.data).toHaveLength(0); - }); - - nock(makeUrl('User', 'create')) - .post(/.*/) - .reply(200, () => { - data.push({ id: '1', email: 'foo' }); - return { data: data[0] }; + const { result: mutationResult } = renderHook(() => useClientQueries(schema).post.useCreate(), { + wrapper, }); - const { result: mutationResult } = renderHook(() => useClientQueries(schema).user.useCreate(), { - wrapper, + act(() => mutationResult.current.mutate({ data: { title: 'post1' } })); + + await waitFor(() => { + // no refetch caused by invalidation + expect(queryCount).toBe(1); + }); }); - act(() => mutationResult.current.mutate({ data: { email: 'foo' } })); + it('works with create and invalidation', async () => { + const { queryClient, wrapper } = createWrapper(); - await waitFor(() => { - const cacheData = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined)); - expect(cacheData).toHaveLength(0); - }); - }); + const data: any[] = []; - it('works with optimistic create single', async () => { - const { queryClient, wrapper } = createWrapper(); + nock(makeUrl('User', 'findMany')) + .get(/.*/) + .reply(200, () => ({ data })) + .persist(); - const data: any[] = []; - - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => ({ data })) - .persist(); - - const { result } = renderHook( - () => useClientQueries(schema).user.useFindMany(undefined, { optimisticUpdate: true }), - { + const { result } = renderHook(() => useClientQueries(schema).user.useFindMany(), { wrapper, - }, - ); - await waitFor(() => { - expect(result.current.data).toHaveLength(0); + }); + await waitFor(() => { + expect(result.current.data).toHaveLength(0); + }); + + nock(makeUrl('User', 'create')) + .post(/.*/) + .reply(200, () => { + data.push({ id: '1', email: 'foo' }); + return { data: data[0] }; + }); + + const { result: mutationResult } = renderHook(() => useClientQueries(schema).user.useCreate(), { + wrapper, + }); + + act(() => mutationResult.current.mutate({ data: { email: 'foo' } })); + + await waitFor(() => { + const cacheData = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined)); + expect(cacheData).toHaveLength(1); + }); }); - nock(makeUrl('User', 'create')) - .post(/.*/) - .reply(200, () => ({ - data: null, - })); + it('works with create and no invalidation', async () => { + const { queryClient, wrapper } = createWrapper(); - const { result: mutationResult } = renderHook( - () => - useClientQueries(schema).user.useCreate({ - optimisticUpdate: true, - invalidateQueries: false, - }), - { + const data: any[] = []; + + nock(makeUrl('User', 'findMany')) + .get(/.*/) + .reply(200, () => ({ data })) + .persist(); + + const { result } = renderHook(() => useClientQueries(schema).user.useFindMany(), { wrapper, - }, - ); + }); + await waitFor(() => { + expect(result.current.data).toHaveLength(0); + }); - act(() => mutationResult.current.mutate({ data: { email: 'foo' } })); + nock(makeUrl('User', 'create')) + .post(/.*/) + .reply(200, () => { + data.push({ id: '1', email: 'foo' }); + return { data: data[0] }; + }); - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }), + const { result: mutationResult } = renderHook(() => useClientQueries(schema).user.useCreate(), { + wrapper, + }); + + act(() => mutationResult.current.mutate({ data: { email: 'foo' } })); + + await waitFor(() => { + const cacheData = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined)); + expect(cacheData).toHaveLength(0); + }); + }); + + it('works with update and invalidation', async () => { + const { queryClient, wrapper } = createWrapper(); + + const queryArgs = { where: { id: '1' } }; + const data = { id: '1', name: 'foo' }; + + nock(makeUrl('User', 'findUnique', queryArgs)) + .get(/.*/) + .reply(200, () => ({ + data, + })) + .persist(); + + const { result } = renderHook(() => useClientQueries(schema).user.useFindUnique(queryArgs), { + wrapper, + }); + await waitFor(() => { + expect(result.current.data).toMatchObject({ name: 'foo' }); + }); + + nock(makeUrl('User', 'update')) + .put(/.*/) + .reply(200, () => { + data.name = 'bar'; + return data; + }); + + const { result: mutationResult } = renderHook(() => useClientQueries(schema).user.useUpdate(), { + wrapper, + }); + + act(() => mutationResult.current.mutate({ ...queryArgs, data: { name: 'bar' } })); + + await waitFor(() => { + const cacheData = queryClient.getQueryData(getQueryKey('User', 'findUnique', queryArgs)); + expect(cacheData).toMatchObject({ name: 'bar' }); + }); + }); + + it('works with update and no invalidation', async () => { + const { queryClient, wrapper } = createWrapper(); + + const queryArgs = { where: { id: '1' } }; + const data = { id: '1', name: 'foo' }; + + nock(makeUrl('User', 'findUnique', queryArgs)) + .get(/.*/) + .reply(200, () => ({ data })) + .persist(); + + const { result } = renderHook(() => useClientQueries(schema).user.useFindUnique(queryArgs), { + wrapper, + }); + await waitFor(() => { + expect(result.current.data).toMatchObject({ name: 'foo' }); + }); + + nock(makeUrl('User', 'update')) + .put(/.*/) + .reply(200, () => { + data.name = 'bar'; + return data; + }); + + const { result: mutationResult } = renderHook(() => useClientQueries(schema).user.useUpdate(), { + wrapper, + }); + + act(() => mutationResult.current.mutate({ ...queryArgs, data: { name: 'bar' } })); + + await waitFor(() => { + const cacheData = queryClient.getQueryData(getQueryKey('User', 'findUnique', queryArgs)); + expect(cacheData).toMatchObject({ name: 'foo' }); + }); + }); + + it('works with delete and invalidation', async () => { + const { queryClient, wrapper } = createWrapper(); + + const data: any[] = [{ id: '1', name: 'foo' }]; + + nock(makeUrl('User', 'findMany')) + .get(/.*/) + .reply(200, () => ({ data })) + .persist(); + + const { result } = renderHook(() => useClientQueries(schema).user.useFindMany(), { + wrapper, + }); + await waitFor(() => { + expect(result.current.data).toHaveLength(1); + }); + + nock(makeUrl('User', 'delete')) + .delete(/.*/) + .reply(200, () => { + data.splice(0, 1); + return { data: [] }; + }); + + const { result: mutationResult } = renderHook(() => useClientQueries(schema).user.useDelete(), { + wrapper, + }); + + act(() => mutationResult.current.mutate({ where: { id: '1' } })); + + await waitFor(() => { + const cacheData = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined)); + expect(cacheData).toHaveLength(0); + }); + }); + + it('top-level mutation and nested-read invalidation', async () => { + const { queryClient, wrapper } = createWrapper(); + + const queryArgs = { where: { id: '1' }, include: { posts: true } }; + const data = { posts: [{ id: '1', title: 'post1' }] }; + + nock(makeUrl('User', 'findUnique', queryArgs)) + .get(/.*/) + .reply(200, () => ({ data })) + .persist(); + + const { result } = renderHook(() => useClientQueries(schema).user.useFindUnique(queryArgs), { + wrapper, + }); + await waitFor(() => { + expect(result.current.data).toMatchObject(data); + }); + + nock(makeUrl('Post', 'update')) + .put(/.*/) + .reply(200, () => { + data.posts[0]!.title = 'post2'; + return data; + }); + + const { result: mutationResult } = renderHook(() => useClientQueries(schema).post.useUpdate(), { + wrapper, + }); + + act(() => mutationResult.current.mutate({ where: { id: '1' }, data: { title: 'post2' } })); + + await waitFor(() => { + const cacheData: any = queryClient.getQueryData(getQueryKey('User', 'findUnique', queryArgs)); + expect(cacheData.posts[0].title).toBe('post2'); + }); + }); + + it('nested mutation and top-level-read invalidation', async () => { + const { queryClient, wrapper } = createWrapper(); + + const data = [{ id: '1', title: 'post1', ownerId: '1' }]; + + nock(makeUrl('Post', 'findMany')) + .get(/.*/) + .reply(200, () => ({ + data, + })) + .persist(); + + const { result } = renderHook(() => useClientQueries(schema).post.useFindMany(), { + wrapper, + }); + await waitFor(() => { + expect(result.current.data).toMatchObject(data); + }); + + nock(makeUrl('User', 'update')) + .put(/.*/) + .reply(200, () => { + data.push({ id: '2', title: 'post2', ownerId: '1' }); + return data; + }); + + const { result: mutationResult } = renderHook(() => useClientQueries(schema).user.useUpdate(), { + wrapper, + }); + + act(() => + mutationResult.current.mutate({ where: { id: '1' }, data: { posts: { create: { title: 'post2' } } } }), ); - expect(cacheData).toHaveLength(1); - expect(cacheData[0].$optimistic).toBe(true); - expect(cacheData[0].id).toBeTruthy(); - expect(cacheData[0].email).toBe('foo'); + + await waitFor(() => { + const cacheData: any = queryClient.getQueryData(getQueryKey('Post', 'findMany', undefined)); + expect(cacheData).toHaveLength(2); + }); }); }); - it('works with optimistic create updating nested query', async () => { - const { queryClient, wrapper } = createWrapper(); + describe('Optimistic mutation', () => { + it('works with optimistic create single', async () => { + const { queryClient, wrapper } = createWrapper(); - const data: any[] = [{ id: '1', name: 'user1', posts: [] }]; + const data: any[] = []; - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => ({ data })) - .persist(); + nock(makeUrl('User', 'findMany')) + .get(/.*/) + .reply(200, () => ({ data })) + .persist(); - const { result } = renderHook( - () => - useClientQueries(schema).user.useFindMany( - { - include: { posts: true }, - }, - { optimisticUpdate: true }, - ), - { - wrapper, - }, - ); - await waitFor(() => { - expect(result.current.data).toHaveLength(1); - }); - - nock(makeUrl('Post', 'create')) - .post(/.*/) - .reply(200, () => ({ - data: null, - })); - - const { result: mutationResult } = renderHook( - () => - useClientQueries(schema).post.useCreate({ - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - }, - ); - - act(() => mutationResult.current.mutate({ data: { title: 'post1', owner: { connect: { id: '1' } } } })); - - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey( - 'User', - 'findMany', - { include: { posts: true } }, - { infinite: false, optimisticUpdate: true }, - ), + const { result } = renderHook( + () => useClientQueries(schema).user.useFindMany(undefined, { optimisticUpdate: true }), + { + wrapper, + }, ); - const posts = cacheData[0].posts; - expect(posts).toHaveLength(1); - expect(posts[0]).toMatchObject({ $optimistic: true, id: expect.any(String), title: 'post1', ownerId: '1' }); + await waitFor(() => { + expect(result.current.data).toHaveLength(0); + }); + + nock(makeUrl('User', 'create')) + .post(/.*/) + .reply(200, () => ({ + data: null, + })); + + const { result: mutationResult } = renderHook( + () => + useClientQueries(schema).user.useCreate({ + optimisticUpdate: true, + invalidateQueries: false, + }), + { + wrapper, + }, + ); + + act(() => mutationResult.current.mutate({ data: { email: 'foo' } })); + + await waitFor(() => { + const cacheData: any = queryClient.getQueryData( + getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }), + ); + expect(cacheData).toHaveLength(1); + expect(cacheData[0].$optimistic).toBe(true); + expect(cacheData[0].id).toBeTruthy(); + expect(cacheData[0].email).toBe('foo'); + }); }); - }); - it('works with optimistic create updating deeply nested query', async () => { - const { queryClient, wrapper } = createWrapper(); + it('works with optimistic create updating nested query', async () => { + const { queryClient, wrapper } = createWrapper(); - // populate the cache with a user + const data: any[] = [{ id: '1', name: 'user1', posts: [] }]; - const userData: any[] = [{ id: '1', email: 'user1', posts: [] }]; + nock(makeUrl('User', 'findMany')) + .get(/.*/) + .reply(200, () => ({ data })) + .persist(); - nock(BASE_URL) - .get('/api/model/user/findMany') - .query(true) - .reply(200, () => ({ data: userData })) - .persist(); + const { result } = renderHook( + () => + useClientQueries(schema).user.useFindMany( + { + include: { posts: true }, + }, + { optimisticUpdate: true }, + ), + { + wrapper, + }, + ); + await waitFor(() => { + expect(result.current.data).toHaveLength(1); + }); - const { result: userResult } = renderHook( - () => - useClientQueries(schema).user.useFindMany( - { - include: { - posts: { - include: { - category: true, + nock(makeUrl('Post', 'create')) + .post(/.*/) + .reply(200, () => ({ + data: null, + })); + + const { result: mutationResult } = renderHook( + () => + useClientQueries(schema).post.useCreate({ + optimisticUpdate: true, + invalidateQueries: false, + }), + { + wrapper, + }, + ); + + act(() => mutationResult.current.mutate({ data: { title: 'post1', owner: { connect: { id: '1' } } } })); + + await waitFor(() => { + const cacheData: any = queryClient.getQueryData( + getQueryKey( + 'User', + 'findMany', + { include: { posts: true } }, + { infinite: false, optimisticUpdate: true }, + ), + ); + const posts = cacheData[0].posts; + expect(posts).toHaveLength(1); + expect(posts[0]).toMatchObject({ $optimistic: true, id: expect.any(String), title: 'post1', ownerId: '1' }); + }); + }); + + it('works with optimistic create updating deeply nested query', async () => { + const { queryClient, wrapper } = createWrapper(); + + // populate the cache with a user + + const userData: any[] = [{ id: '1', email: 'user1', posts: [] }]; + + nock(BASE_URL) + .get('/api/model/user/findMany') + .query(true) + .reply(200, () => ({ data: userData })) + .persist(); + + const { result: userResult } = renderHook( + () => + useClientQueries(schema).user.useFindMany( + { + include: { + posts: { + include: { + category: true, + }, }, }, }, - }, - { optimisticUpdate: true }, - ), - { - wrapper, - }, - ); - await waitFor(() => { - expect(userResult.current.data).toHaveLength(1); - }); - - // populate the cache with a category - const categoryData: any[] = [{ id: '1', name: 'category1', posts: [] }]; - - nock(BASE_URL) - .get('/api/model/category/findMany') - .query(true) - .reply(200, () => ({ data: categoryData })) - .persist(); - - const { result: categoryResult } = renderHook( - () => - useClientQueries(schema).category.useFindMany( - { - include: { - posts: true, - }, - }, - { optimisticUpdate: true }, - ), - { - wrapper, - }, - ); - await waitFor(() => { - expect(categoryResult.current.data).toHaveLength(1); - }); - - // create a post and connect it to the category - nock(BASE_URL) - .post('/api/model/post/create') - .reply(200, () => ({ - data: null, - })); - - const { result: mutationResult } = renderHook( - () => - useClientQueries(schema).post.useCreate({ - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - }, - ); - - act(() => - mutationResult.current.mutate({ - data: { title: 'post1', owner: { connect: { id: '1' } }, category: { connect: { id: '1' } } }, - }), - ); - - // assert that the post was created and connected to the category - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey( - 'Category', - 'findMany', - { - include: { - posts: true, - }, - }, - { infinite: false, optimisticUpdate: true }, - ), + { optimisticUpdate: true }, + ), + { + wrapper, + }, ); - const posts = cacheData[0].posts; - expect(posts).toHaveLength(1); - expect(posts[0]).toMatchObject({ - $optimistic: true, - id: expect.any(String), - title: 'post1', - ownerId: '1', + await waitFor(() => { + expect(userResult.current.data).toHaveLength(1); }); - }); - // assert that the post was created and connected to the user, and included the category - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey( - 'User', - 'findMany', - { - include: { - posts: { - include: { - category: true, + // populate the cache with a category + const categoryData: any[] = [{ id: '1', name: 'category1', posts: [] }]; + + nock(BASE_URL) + .get('/api/model/category/findMany') + .query(true) + .reply(200, () => ({ data: categoryData })) + .persist(); + + const { result: categoryResult } = renderHook( + () => + useClientQueries(schema).category.useFindMany( + { + include: { + posts: true, + }, + }, + { optimisticUpdate: true }, + ), + { + wrapper, + }, + ); + await waitFor(() => { + expect(categoryResult.current.data).toHaveLength(1); + }); + + // create a post and connect it to the category + nock(BASE_URL) + .post('/api/model/post/create') + .reply(200, () => ({ + data: null, + })); + + const { result: mutationResult } = renderHook( + () => + useClientQueries(schema).post.useCreate({ + optimisticUpdate: true, + invalidateQueries: false, + }), + { + wrapper, + }, + ); + + act(() => + mutationResult.current.mutate({ + data: { title: 'post1', owner: { connect: { id: '1' } }, category: { connect: { id: '1' } } }, + }), + ); + + // assert that the post was created and connected to the category + await waitFor(() => { + const cacheData: any = queryClient.getQueryData( + getQueryKey( + 'Category', + 'findMany', + { + include: { + posts: true, + }, + }, + { infinite: false, optimisticUpdate: true }, + ), + ); + const posts = cacheData[0].posts; + expect(posts).toHaveLength(1); + expect(posts[0]).toMatchObject({ + $optimistic: true, + id: expect.any(String), + title: 'post1', + ownerId: '1', + }); + }); + + // assert that the post was created and connected to the user, and included the category + await waitFor(() => { + const cacheData: any = queryClient.getQueryData( + getQueryKey( + 'User', + 'findMany', + { + include: { + posts: { + include: { + category: true, + }, }, }, }, - }, - { infinite: false, optimisticUpdate: true }, - ), - ); - const posts = cacheData[0].posts; - expect(posts).toHaveLength(1); - expect(posts[0]).toMatchObject({ - $optimistic: true, - id: expect.any(String), + { infinite: false, optimisticUpdate: true }, + ), + ); + const posts = cacheData[0].posts; + expect(posts).toHaveLength(1); + expect(posts[0]).toMatchObject({ + $optimistic: true, + id: expect.any(String), + title: 'post1', + ownerId: '1', + categoryId: '1', + // TODO: should this include the category object and not just the foreign key? + // category: { $optimistic: true, id: '1', name: 'category1' }, + }); + }); + }); + + it('works with optimistic update with optional one-to-many relationship', async () => { + const { queryClient, wrapper } = createWrapper(); + + // populate the cache with a post, with an optional category relationship + const postData: any = { + id: '1', title: 'post1', ownerId: '1', - categoryId: '1', - // TODO: should this include the category object and not just the foreign key? - // category: { $optimistic: true, id: '1', name: 'category1' }, - }); - }); - }); - - it('works with optimistic update with optional one-to-many relationship', async () => { - const { queryClient, wrapper } = createWrapper(); - - // populate the cache with a post, with an optional category relationship - const postData: any = { - id: '1', - title: 'post1', - ownerId: '1', - categoryId: null, - category: null, - }; - - const data: any[] = [postData]; - - nock(makeUrl('Post', 'findMany')) - .get(/.*/) - .query(true) - .reply(200, () => ({ - data, - })) - .persist(); - - const { result: postResult } = renderHook( - () => - useClientQueries(schema).post.useFindMany( - { - include: { - category: true, - }, - }, - { optimisticUpdate: true }, - ), - { - wrapper, - }, - ); - await waitFor(() => { - expect(postResult.current.data).toHaveLength(1); - }); - - // mock a put request to update the post title - nock(makeUrl('Post', 'update')) - .put(/.*/) - .reply(200, () => { - postData.title = 'postA'; - return { data: postData }; - }); - - const { result: mutationResult } = renderHook( - () => - useClientQueries(schema).post.useUpdate({ - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - }, - ); - - act(() => mutationResult.current.mutate({ where: { id: '1' }, data: { title: 'postA' } })); - - // assert that the post was updated despite the optional (null) category relationship - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey( - 'Post', - 'findMany', - { - include: { - category: true, - }, - }, - { infinite: false, optimisticUpdate: true }, - ), - ); - const posts = cacheData; - expect(posts).toHaveLength(1); - expect(posts[0]).toMatchObject({ - $optimistic: true, - id: expect.any(String), - title: 'postA', - ownerId: '1', categoryId: null, category: null, - }); - }); - }); + }; - it('works with optimistic update with nested optional one-to-many relationship', async () => { - const { queryClient, wrapper } = createWrapper(); + const data: any[] = [postData]; - // populate the cache with a user and a post, with an optional category - const postData: any = { - id: '1', - title: 'post1', - ownerId: '1', - categoryId: null, - category: null, - }; + nock(makeUrl('Post', 'findMany')) + .get(/.*/) + .query(true) + .reply(200, () => ({ + data, + })) + .persist(); - const userData: any[] = [{ id: '1', name: 'user1', posts: [postData] }]; - - nock(BASE_URL) - .get('/api/model/user/findMany') - .query(true) - .reply(200, () => { - return { data: userData }; - }) - .persist(); - - const { result: userResult } = renderHook( - () => - useClientQueries(schema).user.useFindMany( - { - include: { - posts: { - include: { - category: true, - }, + const { result: postResult } = renderHook( + () => + useClientQueries(schema).post.useFindMany( + { + include: { + category: true, }, }, - }, - { optimisticUpdate: true }, - ), - { - wrapper, - }, - ); - await waitFor(() => { - expect(userResult.current.data).toHaveLength(1); - }); - - // mock a put request to update the post title - nock(BASE_URL) - .put('/api/model/post/update') - .reply(200, () => { - postData.title = 'postA'; - return { data: postData }; - }); - - const { result: mutationResult } = renderHook( - () => - useClientQueries(schema).post.useUpdate({ - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - }, - ); - - act(() => mutationResult.current.mutate({ where: { id: '1' }, data: { title: 'postA' } })); - - // assert that the post was updated - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey( - 'User', - 'findMany', - { - include: { - posts: { - include: { - category: true, - }, - }, - }, - }, - { infinite: false, optimisticUpdate: true }, - ), + { optimisticUpdate: true }, + ), + { + wrapper, + }, ); - const posts = cacheData[0].posts; - expect(posts).toHaveLength(1); - expect(posts[0]).toMatchObject({ - $optimistic: true, - id: expect.any(String), - title: 'postA', + await waitFor(() => { + expect(postResult.current.data).toHaveLength(1); + }); + + // mock a put request to update the post title + nock(makeUrl('Post', 'update')) + .put(/.*/) + .reply(200, () => { + postData.title = 'postA'; + return { data: postData }; + }); + + const { result: mutationResult } = renderHook( + () => + useClientQueries(schema).post.useUpdate({ + optimisticUpdate: true, + invalidateQueries: false, + }), + { + wrapper, + }, + ); + + act(() => mutationResult.current.mutate({ where: { id: '1' }, data: { title: 'postA' } })); + + // assert that the post was updated despite the optional (null) category relationship + await waitFor(() => { + const cacheData: any = queryClient.getQueryData( + getQueryKey( + 'Post', + 'findMany', + { + include: { + category: true, + }, + }, + { infinite: false, optimisticUpdate: true }, + ), + ); + const posts = cacheData; + expect(posts).toHaveLength(1); + expect(posts[0]).toMatchObject({ + $optimistic: true, + id: expect.any(String), + title: 'postA', + ownerId: '1', + categoryId: null, + category: null, + }); + }); + }); + + it('works with optimistic update with nested optional one-to-many relationship', async () => { + const { queryClient, wrapper } = createWrapper(); + + // populate the cache with a user and a post, with an optional category + const postData: any = { + id: '1', + title: 'post1', ownerId: '1', categoryId: null, category: null, - }); - }); - }); + }; - it('works with optimistic nested create updating query', async () => { - const { queryClient, wrapper } = createWrapper(); + const userData: any[] = [{ id: '1', name: 'user1', posts: [postData] }]; - const data: any[] = []; + nock(BASE_URL) + .get('/api/model/user/findMany') + .query(true) + .reply(200, () => { + return { data: userData }; + }) + .persist(); - nock(makeUrl('Post', 'findMany')) - .get(/.*/) - .reply(200, () => ({ - data, - })) - .persist(); - - const { result } = renderHook( - () => useClientQueries(schema).post.useFindMany(undefined, { optimisticUpdate: true }), - { - wrapper, - }, - ); - await waitFor(() => { - expect(result.current.data).toHaveLength(0); - }); - - nock(makeUrl('User', 'create')) - .post(/.*/) - .reply(200, () => ({ - data: null, - })); - - const { result: mutationResult } = renderHook( - () => - useClientQueries(schema).user.useCreate({ - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - }, - ); - - act(() => mutationResult.current.mutate({ data: { email: 'user1', posts: { create: { title: 'post1' } } } })); - - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey('Post', 'findMany', undefined, { infinite: false, optimisticUpdate: true }), + const { result: userResult } = renderHook( + () => + useClientQueries(schema).user.useFindMany( + { + include: { + posts: { + include: { + category: true, + }, + }, + }, + }, + { optimisticUpdate: true }, + ), + { + wrapper, + }, ); - expect(cacheData).toHaveLength(1); - expect(cacheData[0].$optimistic).toBe(true); - expect(cacheData[0].id).toBeTruthy(); - expect(cacheData[0].title).toBe('post1'); - }); - }); - - it('works with optimistic create many', async () => { - const { queryClient, wrapper } = createWrapper(); - - const data: any[] = []; - - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => ({ - data, - })) - .persist(); - - const { result } = renderHook( - () => useClientQueries(schema).user.useFindMany(undefined, { optimisticUpdate: true }), - { - wrapper, - }, - ); - await waitFor(() => { - expect(result.current.data).toHaveLength(0); - }); - - nock(makeUrl('User', 'createMany')) - .post(/.*/) - .reply(200, () => ({ - data: null, - })); - - const { result: mutationResult } = renderHook( - () => - useClientQueries(schema).user.useCreateMany({ - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - }, - ); - - act(() => mutationResult.current.mutate({ data: [{ email: 'foo' }, { email: 'bar' }] })); - - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }), - ); - expect(cacheData).toHaveLength(2); - }); - }); - - it('works with update and invalidation', async () => { - const { queryClient, wrapper } = createWrapper(); - - const queryArgs = { where: { id: '1' } }; - const data = { id: '1', name: 'foo' }; - - nock(makeUrl('User', 'findUnique', queryArgs)) - .get(/.*/) - .reply(200, () => ({ - data, - })) - .persist(); - - const { result } = renderHook(() => useClientQueries(schema).user.useFindUnique(queryArgs), { - wrapper, - }); - await waitFor(() => { - expect(result.current.data).toMatchObject({ name: 'foo' }); - }); - - nock(makeUrl('User', 'update')) - .put(/.*/) - .reply(200, () => { - data.name = 'bar'; - return data; + await waitFor(() => { + expect(userResult.current.data).toHaveLength(1); }); - const { result: mutationResult } = renderHook(() => useClientQueries(schema).user.useUpdate(), { - wrapper, + // mock a put request to update the post title + nock(BASE_URL) + .put('/api/model/post/update') + .reply(200, () => { + postData.title = 'postA'; + return { data: postData }; + }); + + const { result: mutationResult } = renderHook( + () => + useClientQueries(schema).post.useUpdate({ + optimisticUpdate: true, + invalidateQueries: false, + }), + { + wrapper, + }, + ); + + act(() => mutationResult.current.mutate({ where: { id: '1' }, data: { title: 'postA' } })); + + // assert that the post was updated + await waitFor(() => { + const cacheData: any = queryClient.getQueryData( + getQueryKey( + 'User', + 'findMany', + { + include: { + posts: { + include: { + category: true, + }, + }, + }, + }, + { infinite: false, optimisticUpdate: true }, + ), + ); + const posts = cacheData[0].posts; + expect(posts).toHaveLength(1); + expect(posts[0]).toMatchObject({ + $optimistic: true, + id: expect.any(String), + title: 'postA', + ownerId: '1', + categoryId: null, + category: null, + }); + }); }); - act(() => mutationResult.current.mutate({ ...queryArgs, data: { name: 'bar' } })); + it('works with optimistic nested create updating query', async () => { + const { queryClient, wrapper } = createWrapper(); - await waitFor(() => { - const cacheData = queryClient.getQueryData(getQueryKey('User', 'findUnique', queryArgs)); - expect(cacheData).toMatchObject({ name: 'bar' }); - }); - }); + const data: any[] = []; - it('works with update and no invalidation', async () => { - const { queryClient, wrapper } = createWrapper(); + nock(makeUrl('Post', 'findMany')) + .get(/.*/) + .reply(200, () => ({ + data, + })) + .persist(); - const queryArgs = { where: { id: '1' } }; - const data = { id: '1', name: 'foo' }; - - nock(makeUrl('User', 'findUnique', queryArgs)) - .get(/.*/) - .reply(200, () => ({ data })) - .persist(); - - const { result } = renderHook(() => useClientQueries(schema).user.useFindUnique(queryArgs), { - wrapper, - }); - await waitFor(() => { - expect(result.current.data).toMatchObject({ name: 'foo' }); - }); - - nock(makeUrl('User', 'update')) - .put(/.*/) - .reply(200, () => { - data.name = 'bar'; - return data; + const { result } = renderHook( + () => useClientQueries(schema).post.useFindMany(undefined, { optimisticUpdate: true }), + { + wrapper, + }, + ); + await waitFor(() => { + expect(result.current.data).toHaveLength(0); }); - const { result: mutationResult } = renderHook(() => useClientQueries(schema).user.useUpdate(), { - wrapper, - }); + nock(makeUrl('User', 'create')) + .post(/.*/) + .reply(200, () => ({ + data: null, + })); - act(() => mutationResult.current.mutate({ ...queryArgs, data: { name: 'bar' } })); - - await waitFor(() => { - const cacheData = queryClient.getQueryData(getQueryKey('User', 'findUnique', queryArgs)); - expect(cacheData).toMatchObject({ name: 'foo' }); - }); - }); - - it('works with optimistic update simple', async () => { - const { queryClient, wrapper } = createWrapper(); - - const queryArgs = { where: { id: '1' } }; - const data = { id: '1', name: 'foo' }; - - nock(makeUrl('User', 'findUnique', queryArgs)) - .get(/.*/) - .reply(200, () => ({ - data, - })) - .persist(); - - const { result } = renderHook( - () => useClientQueries(schema).user.useFindUnique(queryArgs, { optimisticUpdate: true }), - { - wrapper, - }, - ); - await waitFor(() => { - expect(result.current.data).toMatchObject({ name: 'foo' }); - }); - - nock(makeUrl('User', 'update')) - .put(/.*/) - .reply(200, () => data); - - const { result: mutationResult } = renderHook( - () => - useClientQueries(schema).user.useUpdate({ - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - }, - ); - - act(() => mutationResult.current.mutate({ ...queryArgs, data: { name: 'bar' } })); - - await waitFor(() => { - const cacheData = queryClient.getQueryData( - getQueryKey('User', 'findUnique', queryArgs, { infinite: false, optimisticUpdate: true }), + const { result: mutationResult } = renderHook( + () => + useClientQueries(schema).user.useCreate({ + optimisticUpdate: true, + invalidateQueries: false, + }), + { + wrapper, + }, ); - expect(cacheData).toMatchObject({ name: 'bar', $optimistic: true }); - }); - }); - it('works with optimistic update updating nested query', async () => { - const { queryClient, wrapper } = createWrapper(); + act(() => mutationResult.current.mutate({ data: { email: 'user1', posts: { create: { title: 'post1' } } } })); - const queryArgs = { where: { id: '1' }, include: { posts: true } }; - const data = { id: '1', name: 'foo', posts: [{ id: 'p1', title: 'post1' }] }; - - nock(makeUrl('User', 'findUnique', queryArgs)) - .get(/.*/) - .reply(200, () => ({ data })) - .persist(); - - const { result } = renderHook( - () => useClientQueries(schema).user.useFindUnique(queryArgs, { optimisticUpdate: true }), - { - wrapper, - }, - ); - await waitFor(() => { - expect(result.current.data).toMatchObject({ name: 'foo' }); + await waitFor(() => { + const cacheData: any = queryClient.getQueryData( + getQueryKey('Post', 'findMany', undefined, { infinite: false, optimisticUpdate: true }), + ); + expect(cacheData).toHaveLength(1); + expect(cacheData[0].$optimistic).toBe(true); + expect(cacheData[0].id).toBeTruthy(); + expect(cacheData[0].title).toBe('post1'); + }); }); - nock(makeUrl('Post', 'update')) - .put(/.*/) - .reply(200, () => data); + it('works with optimistic create many', async () => { + const { queryClient, wrapper } = createWrapper(); - const { result: mutationResult } = renderHook( - () => - useClientQueries(schema).post.useUpdate({ - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - }, - ); + const data: any[] = []; - act(() => - mutationResult.current.mutate({ - where: { id: 'p1' }, - data: { title: 'post2', owner: { connect: { id: '2' } } }, - }), - ); + nock(makeUrl('User', 'findMany')) + .get(/.*/) + .reply(200, () => ({ + data, + })) + .persist(); - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey('User', 'findUnique', queryArgs, { infinite: false, optimisticUpdate: true }), + const { result } = renderHook( + () => useClientQueries(schema).user.useFindMany(undefined, { optimisticUpdate: true }), + { + wrapper, + }, ); - expect(cacheData.posts[0]).toMatchObject({ title: 'post2', $optimistic: true, ownerId: '2' }); - }); - }); + await waitFor(() => { + expect(result.current.data).toHaveLength(0); + }); - it('works with optimistic nested update updating query', async () => { - const { queryClient, wrapper } = createWrapper(); + nock(makeUrl('User', 'createMany')) + .post(/.*/) + .reply(200, () => ({ + data: null, + })); - const queryArgs = { where: { id: 'p1' } }; - const data = { id: 'p1', title: 'post1' }; - - nock(makeUrl('Post', 'findUnique', queryArgs)) - .get(/.*/) - .reply(200, () => ({ data })) - .persist(); - - const { result } = renderHook( - () => useClientQueries(schema).post.useFindUnique(queryArgs, { optimisticUpdate: true }), - { - wrapper, - }, - ); - await waitFor(() => { - expect(result.current.data).toMatchObject({ title: 'post1' }); - }); - - nock(makeUrl('User', 'update')) - .put(/.*/) - .reply(200, () => data); - - const { result: mutationResult } = renderHook( - () => - useClientQueries(schema).user.useUpdate({ - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - }, - ); - - act(() => - mutationResult.current.mutate({ - where: { id: '1' }, - data: { posts: { update: { where: { id: 'p1' }, data: { title: 'post2' } } } }, - }), - ); - - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey('Post', 'findUnique', queryArgs, { infinite: false, optimisticUpdate: true }), + const { result: mutationResult } = renderHook( + () => + useClientQueries(schema).user.useCreateMany({ + optimisticUpdate: true, + invalidateQueries: false, + }), + { + wrapper, + }, ); - expect(cacheData).toMatchObject({ title: 'post2', $optimistic: true }); - }); - }); - it('works with optimistic upsert - create simple', async () => { - const { queryClient, wrapper } = createWrapper(); + act(() => mutationResult.current.mutate({ data: [{ email: 'foo' }, { email: 'bar' }] })); - const data: any[] = []; - - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => ({ data })) - .persist(); - - const { result } = renderHook( - () => useClientQueries(schema).user.useFindMany(undefined, { optimisticUpdate: true }), - { - wrapper, - }, - ); - await waitFor(() => { - expect(result.current.data).toHaveLength(0); + await waitFor(() => { + const cacheData: any = queryClient.getQueryData( + getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }), + ); + expect(cacheData).toHaveLength(2); + }); }); - nock(makeUrl('User', 'upsert')) - .post(/.*/) - .reply(200, () => ({ data: null })); + it('works with optimistic update simple', async () => { + const { queryClient, wrapper } = createWrapper(); - const { result: mutationResult } = renderHook( - () => - useClientQueries(schema).user.useUpsert({ - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - }, - ); + const queryArgs = { where: { id: '1' } }; + const data = { id: '1', name: 'foo' }; - act(() => - mutationResult.current.mutate({ - where: { id: '1' }, - create: { id: '1', email: 'foo' }, - update: { email: 'bar' }, - }), - ); + nock(makeUrl('User', 'findUnique', queryArgs)) + .get(/.*/) + .reply(200, () => ({ + data, + })) + .persist(); - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }), + const { result } = renderHook( + () => useClientQueries(schema).user.useFindUnique(queryArgs, { optimisticUpdate: true }), + { + wrapper, + }, ); - expect(cacheData).toHaveLength(1); - expect(cacheData[0]).toMatchObject({ id: '1', email: 'foo', $optimistic: true }); - }); - }); + await waitFor(() => { + expect(result.current.data).toMatchObject({ name: 'foo' }); + }); - it('works with optimistic upsert - create updating nested query', async () => { - const { queryClient, wrapper } = createWrapper(); + nock(makeUrl('User', 'update')) + .put(/.*/) + .reply(200, () => data); - const data: any = { id: '1', name: 'user1', posts: [{ id: 'p1', title: 'post1' }] }; - - nock(makeUrl('User', 'findUnique')) - .get(/.*/) - .reply(200, () => ({ data })) - .persist(); - - const { result } = renderHook( - () => useClientQueries(schema).user.useFindUnique({ where: { id: '1' } }, { optimisticUpdate: true }), - { - wrapper, - }, - ); - await waitFor(() => { - expect(result.current.data).toMatchObject({ id: '1' }); - }); - - nock(makeUrl('Post', 'upsert')) - .post(/.*/) - .reply(200, () => ({ data: null })); - - const { result: mutationResult } = renderHook( - () => - useClientQueries(schema).post.useUpsert({ - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - }, - ); - - act(() => - mutationResult.current.mutate({ - where: { id: 'p2' }, - create: { id: 'p2', title: 'post2', owner: { connect: { id: '1' } } }, - update: { title: 'post3' }, - }), - ); - - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey('User', 'findUnique', { where: { id: '1' } }, { infinite: false, optimisticUpdate: true }), + const { result: mutationResult } = renderHook( + () => + useClientQueries(schema).user.useUpdate({ + optimisticUpdate: true, + invalidateQueries: false, + }), + { + wrapper, + }, ); - const posts = cacheData.posts; - expect(posts).toHaveLength(2); - expect(posts[0]).toMatchObject({ id: 'p2', title: 'post2', ownerId: '1', $optimistic: true }); - }); - }); - it('works with optimistic upsert - nested create updating query', async () => { - const { queryClient, wrapper } = createWrapper(); + act(() => mutationResult.current.mutate({ ...queryArgs, data: { name: 'bar' } })); - const data: any = [{ id: 'p1', title: 'post1' }]; - - nock(makeUrl('Post', 'findMany')) - .get(/.*/) - .reply(200, () => ({ data })) - .persist(); - - const { result } = renderHook( - () => useClientQueries(schema).post.useFindMany(undefined, { optimisticUpdate: true }), - { - wrapper, - }, - ); - await waitFor(() => { - expect(result.current.data).toHaveLength(1); + await waitFor(() => { + const cacheData = queryClient.getQueryData( + getQueryKey('User', 'findUnique', queryArgs, { infinite: false, optimisticUpdate: true }), + ); + expect(cacheData).toMatchObject({ name: 'bar', $optimistic: true }); + }); }); - nock(makeUrl('User', 'update')) - .post(/.*/) - .reply(200, () => ({ data: null })); + it('works with optimistic update updating nested query', async () => { + const { queryClient, wrapper } = createWrapper(); - const { result: mutationResult } = renderHook( - () => - useClientQueries(schema).user.useUpdate({ - optimisticUpdate: true, - invalidateQueries: false, + const queryArgs = { where: { id: '1' }, include: { posts: true } }; + const data = { id: '1', name: 'foo', posts: [{ id: 'p1', title: 'post1' }] }; + + nock(makeUrl('User', 'findUnique', queryArgs)) + .get(/.*/) + .reply(200, () => ({ data })) + .persist(); + + const { result } = renderHook( + () => useClientQueries(schema).user.useFindUnique(queryArgs, { optimisticUpdate: true }), + { + wrapper, + }, + ); + await waitFor(() => { + expect(result.current.data).toMatchObject({ name: 'foo' }); + }); + + nock(makeUrl('Post', 'update')) + .put(/.*/) + .reply(200, () => data); + + const { result: mutationResult } = renderHook( + () => + useClientQueries(schema).post.useUpdate({ + optimisticUpdate: true, + invalidateQueries: false, + }), + { + wrapper, + }, + ); + + act(() => + mutationResult.current.mutate({ + where: { id: 'p1' }, + data: { title: 'post2', owner: { connect: { id: '2' } } }, }), - { - wrapper, - }, - ); + ); - act(() => - mutationResult.current.mutate({ - where: { id: '1' }, - data: { - posts: { - upsert: { - where: { id: 'p2' }, - create: { id: 'p2', title: 'post2' }, - update: { title: 'post3' }, + await waitFor(() => { + const cacheData: any = queryClient.getQueryData( + getQueryKey('User', 'findUnique', queryArgs, { infinite: false, optimisticUpdate: true }), + ); + expect(cacheData.posts[0]).toMatchObject({ title: 'post2', $optimistic: true, ownerId: '2' }); + }); + }); + + it('works with optimistic nested update updating query', async () => { + const { queryClient, wrapper } = createWrapper(); + + const queryArgs = { where: { id: 'p1' } }; + const data = { id: 'p1', title: 'post1' }; + + nock(makeUrl('Post', 'findUnique', queryArgs)) + .get(/.*/) + .reply(200, () => ({ data })) + .persist(); + + const { result } = renderHook( + () => useClientQueries(schema).post.useFindUnique(queryArgs, { optimisticUpdate: true }), + { + wrapper, + }, + ); + await waitFor(() => { + expect(result.current.data).toMatchObject({ title: 'post1' }); + }); + + nock(makeUrl('User', 'update')) + .put(/.*/) + .reply(200, () => data); + + const { result: mutationResult } = renderHook( + () => + useClientQueries(schema).user.useUpdate({ + optimisticUpdate: true, + invalidateQueries: false, + }), + { + wrapper, + }, + ); + + act(() => + mutationResult.current.mutate({ + where: { id: '1' }, + data: { posts: { update: { where: { id: 'p1' }, data: { title: 'post2' } } } }, + }), + ); + + await waitFor(() => { + const cacheData: any = queryClient.getQueryData( + getQueryKey('Post', 'findUnique', queryArgs, { infinite: false, optimisticUpdate: true }), + ); + expect(cacheData).toMatchObject({ title: 'post2', $optimistic: true }); + }); + }); + + it('works with optimistic upsert - create simple', async () => { + const { queryClient, wrapper } = createWrapper(); + + const data: any[] = []; + + nock(makeUrl('User', 'findMany')) + .get(/.*/) + .reply(200, () => ({ data })) + .persist(); + + const { result } = renderHook( + () => useClientQueries(schema).user.useFindMany(undefined, { optimisticUpdate: true }), + { + wrapper, + }, + ); + await waitFor(() => { + expect(result.current.data).toHaveLength(0); + }); + + nock(makeUrl('User', 'upsert')) + .post(/.*/) + .reply(200, () => ({ data: null })); + + const { result: mutationResult } = renderHook( + () => + useClientQueries(schema).user.useUpsert({ + optimisticUpdate: true, + invalidateQueries: false, + }), + { + wrapper, + }, + ); + + act(() => + mutationResult.current.mutate({ + where: { id: '1' }, + create: { id: '1', email: 'foo' }, + update: { email: 'bar' }, + }), + ); + + await waitFor(() => { + const cacheData: any = queryClient.getQueryData( + getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }), + ); + expect(cacheData).toHaveLength(1); + expect(cacheData[0]).toMatchObject({ id: '1', email: 'foo', $optimistic: true }); + }); + }); + + it('works with optimistic upsert - create updating nested query', async () => { + const { queryClient, wrapper } = createWrapper(); + + const data: any = { id: '1', name: 'user1', posts: [{ id: 'p1', title: 'post1' }] }; + + nock(makeUrl('User', 'findUnique')) + .get(/.*/) + .reply(200, () => ({ data })) + .persist(); + + const { result } = renderHook( + () => useClientQueries(schema).user.useFindUnique({ where: { id: '1' } }, { optimisticUpdate: true }), + { + wrapper, + }, + ); + await waitFor(() => { + expect(result.current.data).toMatchObject({ id: '1' }); + }); + + nock(makeUrl('Post', 'upsert')) + .post(/.*/) + .reply(200, () => ({ data: null })); + + const { result: mutationResult } = renderHook( + () => + useClientQueries(schema).post.useUpsert({ + optimisticUpdate: true, + invalidateQueries: false, + }), + { + wrapper, + }, + ); + + act(() => + mutationResult.current.mutate({ + where: { id: 'p2' }, + create: { id: 'p2', title: 'post2', owner: { connect: { id: '1' } } }, + update: { title: 'post3' }, + }), + ); + + await waitFor(() => { + const cacheData: any = queryClient.getQueryData( + getQueryKey('User', 'findUnique', { where: { id: '1' } }, { infinite: false, optimisticUpdate: true }), + ); + const posts = cacheData.posts; + expect(posts).toHaveLength(2); + expect(posts[0]).toMatchObject({ id: 'p2', title: 'post2', ownerId: '1', $optimistic: true }); + }); + }); + + it('works with optimistic upsert - nested create updating query', async () => { + const { queryClient, wrapper } = createWrapper(); + + const data: any = [{ id: 'p1', title: 'post1' }]; + + nock(makeUrl('Post', 'findMany')) + .get(/.*/) + .reply(200, () => ({ data })) + .persist(); + + const { result } = renderHook( + () => useClientQueries(schema).post.useFindMany(undefined, { optimisticUpdate: true }), + { + wrapper, + }, + ); + await waitFor(() => { + expect(result.current.data).toHaveLength(1); + }); + + nock(makeUrl('User', 'update')) + .post(/.*/) + .reply(200, () => ({ data: null })); + + const { result: mutationResult } = renderHook( + () => + useClientQueries(schema).user.useUpdate({ + optimisticUpdate: true, + invalidateQueries: false, + }), + { + wrapper, + }, + ); + + act(() => + mutationResult.current.mutate({ + where: { id: '1' }, + data: { + posts: { + upsert: { + where: { id: 'p2' }, + create: { id: 'p2', title: 'post2' }, + update: { title: 'post3' }, + }, }, }, + }), + ); + + await waitFor(() => { + const cacheData: any = queryClient.getQueryData( + getQueryKey('Post', 'findMany', undefined, { infinite: false, optimisticUpdate: true }), + ); + expect(cacheData).toHaveLength(2); + expect(cacheData[0]).toMatchObject({ id: 'p2', title: 'post2', $optimistic: true }); + }); + }); + + it('works with optimistic upsert - update simple', async () => { + const { queryClient, wrapper } = createWrapper(); + + const queryArgs = { where: { id: '1' } }; + const data = { id: '1', name: 'foo' }; + + nock(makeUrl('User', 'findUnique', queryArgs)) + .get(/.*/) + .reply(200, () => ({ data })) + .persist(); + + const { result } = renderHook( + () => useClientQueries(schema).user.useFindUnique(queryArgs, { optimisticUpdate: true }), + { + wrapper, }, - }), - ); - - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey('Post', 'findMany', undefined, { infinite: false, optimisticUpdate: true }), ); - expect(cacheData).toHaveLength(2); - expect(cacheData[0]).toMatchObject({ id: 'p2', title: 'post2', $optimistic: true }); - }); - }); + await waitFor(() => { + expect(result.current.data).toMatchObject({ name: 'foo' }); + }); - it('works with optimistic upsert - update simple', async () => { - const { queryClient, wrapper } = createWrapper(); + nock(makeUrl('User', 'upsert')) + .post(/.*/) + .reply(200, () => data); - const queryArgs = { where: { id: '1' } }; - const data = { id: '1', name: 'foo' }; - - nock(makeUrl('User', 'findUnique', queryArgs)) - .get(/.*/) - .reply(200, () => ({ data })) - .persist(); - - const { result } = renderHook( - () => useClientQueries(schema).user.useFindUnique(queryArgs, { optimisticUpdate: true }), - { - wrapper, - }, - ); - await waitFor(() => { - expect(result.current.data).toMatchObject({ name: 'foo' }); - }); - - nock(makeUrl('User', 'upsert')) - .post(/.*/) - .reply(200, () => data); - - const { result: mutationResult } = renderHook( - () => - useClientQueries(schema).user.useUpsert({ - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - }, - ); - - act(() => mutationResult.current.mutate({ ...queryArgs, update: { email: 'bar' }, create: { email: 'zee' } })); - - await waitFor(() => { - const cacheData = queryClient.getQueryData( - getQueryKey('User', 'findUnique', queryArgs, { infinite: false, optimisticUpdate: true }), + const { result: mutationResult } = renderHook( + () => + useClientQueries(schema).user.useUpsert({ + optimisticUpdate: true, + invalidateQueries: false, + }), + { + wrapper, + }, ); - expect(cacheData).toMatchObject({ email: 'bar', $optimistic: true }); - }); - }); - it('works with optimistic upsert - update updating nested query', async () => { - const { queryClient, wrapper } = createWrapper(); + act(() => mutationResult.current.mutate({ ...queryArgs, update: { email: 'bar' }, create: { email: 'zee' } })); - const data: any = { id: '1', name: 'user1', posts: [{ id: 'p1', title: 'post1' }] }; - - nock(makeUrl('User', 'findUnique')) - .get(/.*/) - .reply(200, () => ({ data })) - .persist(); - - const { result } = renderHook( - () => useClientQueries(schema).user.useFindUnique({ where: { id: '1' } }, { optimisticUpdate: true }), - { - wrapper, - }, - ); - await waitFor(() => { - expect(result.current.data).toMatchObject({ id: '1' }); + await waitFor(() => { + const cacheData = queryClient.getQueryData( + getQueryKey('User', 'findUnique', queryArgs, { infinite: false, optimisticUpdate: true }), + ); + expect(cacheData).toMatchObject({ email: 'bar', $optimistic: true }); + }); }); - nock(makeUrl('Post', 'upsert')) - .post(/.*/) - .reply(200, () => ({ data: null })); + it('works with optimistic upsert - update updating nested query', async () => { + const { queryClient, wrapper } = createWrapper(); - const { result: mutationResult } = renderHook( - () => - useClientQueries(schema).post.useUpsert({ - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - }, - ); + const data: any = { id: '1', name: 'user1', posts: [{ id: 'p1', title: 'post1' }] }; - act(() => - mutationResult.current.mutate({ - where: { id: 'p1' }, - create: { id: 'p1', title: 'post1' }, - update: { title: 'post2' }, - }), - ); + nock(makeUrl('User', 'findUnique')) + .get(/.*/) + .reply(200, () => ({ data })) + .persist(); - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey('User', 'findUnique', { where: { id: '1' } }, { infinite: false, optimisticUpdate: true }), + const { result } = renderHook( + () => useClientQueries(schema).user.useFindUnique({ where: { id: '1' } }, { optimisticUpdate: true }), + { + wrapper, + }, ); - const posts = cacheData.posts; - expect(posts).toHaveLength(1); - expect(posts[0]).toMatchObject({ id: 'p1', title: 'post2', $optimistic: true }); - }); - }); + await waitFor(() => { + expect(result.current.data).toMatchObject({ id: '1' }); + }); - it('works with optimistic upsert - nested update updating query', async () => { - const { queryClient, wrapper } = createWrapper(); + nock(makeUrl('Post', 'upsert')) + .post(/.*/) + .reply(200, () => ({ data: null })); - const data: any = [{ id: 'p1', title: 'post1' }]; + const { result: mutationResult } = renderHook( + () => + useClientQueries(schema).post.useUpsert({ + optimisticUpdate: true, + invalidateQueries: false, + }), + { + wrapper, + }, + ); - nock(makeUrl('Post', 'findMany')) - .get(/.*/) - .reply(200, () => ({ data })) - .persist(); - - const { result } = renderHook( - () => useClientQueries(schema).post.useFindMany(undefined, { optimisticUpdate: true }), - { - wrapper, - }, - ); - await waitFor(() => { - expect(result.current.data).toHaveLength(1); - }); - - nock(makeUrl('User', 'update')) - .post(/.*/) - .reply(200, () => ({ data: null })); - - const { result: mutationResult } = renderHook( - () => - useClientQueries(schema).user.useUpdate({ - optimisticUpdate: true, - invalidateQueries: false, + act(() => + mutationResult.current.mutate({ + where: { id: 'p1' }, + create: { id: 'p1', title: 'post1' }, + update: { title: 'post2' }, }), - { - wrapper, - }, - ); + ); - act(() => - mutationResult.current.mutate({ - where: { id: '1' }, - data: { - posts: { - upsert: { - where: { id: 'p1' }, - create: { id: 'p1', title: 'post1' }, - update: { title: 'post2' }, + await waitFor(() => { + const cacheData: any = queryClient.getQueryData( + getQueryKey('User', 'findUnique', { where: { id: '1' } }, { infinite: false, optimisticUpdate: true }), + ); + const posts = cacheData.posts; + expect(posts).toHaveLength(1); + expect(posts[0]).toMatchObject({ id: 'p1', title: 'post2', $optimistic: true }); + }); + }); + + it('works with optimistic upsert - nested update updating query', async () => { + const { queryClient, wrapper } = createWrapper(); + + const data: any = [{ id: 'p1', title: 'post1' }]; + + nock(makeUrl('Post', 'findMany')) + .get(/.*/) + .reply(200, () => ({ data })) + .persist(); + + const { result } = renderHook( + () => useClientQueries(schema).post.useFindMany(undefined, { optimisticUpdate: true }), + { + wrapper, + }, + ); + await waitFor(() => { + expect(result.current.data).toHaveLength(1); + }); + + nock(makeUrl('User', 'update')) + .post(/.*/) + .reply(200, () => ({ data: null })); + + const { result: mutationResult } = renderHook( + () => + useClientQueries(schema).user.useUpdate({ + optimisticUpdate: true, + invalidateQueries: false, + }), + { + wrapper, + }, + ); + + act(() => + mutationResult.current.mutate({ + where: { id: '1' }, + data: { + posts: { + upsert: { + where: { id: 'p1' }, + create: { id: 'p1', title: 'post1' }, + update: { title: 'post2' }, + }, }, }, + }), + ); + + await waitFor(() => { + const cacheData: any = queryClient.getQueryData( + getQueryKey('Post', 'findMany', undefined, { infinite: false, optimisticUpdate: true }), + ); + expect(cacheData).toHaveLength(1); + expect(cacheData[0]).toMatchObject({ id: 'p1', title: 'post2', $optimistic: true }); + }); + }); + + it('works with optimistic delete simple', async () => { + const { queryClient, wrapper } = createWrapper(); + + const data: any[] = [{ id: '1', name: 'foo' }]; + + nock(makeUrl('User', 'findMany')) + .get(/.*/) + .reply(200, () => ({ data })) + .persist(); + + const { result } = renderHook( + () => useClientQueries(schema).user.useFindMany(undefined, { optimisticUpdate: true }), + { + wrapper, }, - }), - ); - - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey('Post', 'findMany', undefined, { infinite: false, optimisticUpdate: true }), ); - expect(cacheData).toHaveLength(1); - expect(cacheData[0]).toMatchObject({ id: 'p1', title: 'post2', $optimistic: true }); - }); - }); - - it('works with delete and invalidation', async () => { - const { queryClient, wrapper } = createWrapper(); - - const data: any[] = [{ id: '1', name: 'foo' }]; - - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => ({ data })) - .persist(); - - const { result } = renderHook(() => useClientQueries(schema).user.useFindMany(), { - wrapper, - }); - await waitFor(() => { - expect(result.current.data).toHaveLength(1); - }); - - nock(makeUrl('User', 'delete')) - .delete(/.*/) - .reply(200, () => { - data.splice(0, 1); - return { data: [] }; + await waitFor(() => { + expect(result.current.data).toHaveLength(1); }); - const { result: mutationResult } = renderHook(() => useClientQueries(schema).user.useDelete(), { - wrapper, - }); + nock(makeUrl('User', 'delete')) + .delete(/.*/) + .reply(200, () => ({ data })); - act(() => mutationResult.current.mutate({ where: { id: '1' } })); - - await waitFor(() => { - const cacheData = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined)); - expect(cacheData).toHaveLength(0); - }); - }); - - it('works with optimistic delete simple', async () => { - const { queryClient, wrapper } = createWrapper(); - - const data: any[] = [{ id: '1', name: 'foo' }]; - - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => ({ data })) - .persist(); - - const { result } = renderHook( - () => useClientQueries(schema).user.useFindMany(undefined, { optimisticUpdate: true }), - { - wrapper, - }, - ); - await waitFor(() => { - expect(result.current.data).toHaveLength(1); - }); - - nock(makeUrl('User', 'delete')) - .delete(/.*/) - .reply(200, () => ({ data })); - - const { result: mutationResult } = renderHook( - () => - useClientQueries(schema).user.useDelete({ - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - }, - ); - - act(() => mutationResult.current.mutate({ where: { id: '1' } })); - - await waitFor(() => { - const cacheData = queryClient.getQueryData( - getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }), + const { result: mutationResult } = renderHook( + () => + useClientQueries(schema).user.useDelete({ + optimisticUpdate: true, + invalidateQueries: false, + }), + { + wrapper, + }, ); - expect(cacheData).toHaveLength(0); - }); - }); - it('works with optimistic delete nested query', async () => { - const { queryClient, wrapper } = createWrapper(); + act(() => mutationResult.current.mutate({ where: { id: '1' } })); - const data: any = { id: '1', name: 'foo', posts: [{ id: 'p1', title: 'post1' }] }; - - nock(makeUrl('User', 'findFirst')) - .get(/.*/) - .reply(200, () => ({ data })) - .persist(); - - const { result } = renderHook( - () => - useClientQueries(schema).user.useFindFirst( - { - include: { posts: true }, - }, - { optimisticUpdate: true }, - ), - { - wrapper, - }, - ); - await waitFor(() => { - expect(result.current.data).toMatchObject({ id: '1' }); + await waitFor(() => { + const cacheData = queryClient.getQueryData( + getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }), + ); + expect(cacheData).toHaveLength(0); + }); }); - nock(makeUrl('Post', 'delete')) - .delete(/.*/) - .reply(200, () => ({ data })); + it('works with optimistic delete nested query', async () => { + const { queryClient, wrapper } = createWrapper(); - const { result: mutationResult } = renderHook( - () => - useClientQueries(schema).post.useDelete({ - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - }, - ); + const data: any = { id: '1', name: 'foo', posts: [{ id: 'p1', title: 'post1' }] }; - act(() => mutationResult.current.mutate({ where: { id: 'p1' } })); + nock(makeUrl('User', 'findFirst')) + .get(/.*/) + .reply(200, () => ({ data })) + .persist(); - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey( - 'User', - 'findFirst', - { include: { posts: true } }, - { infinite: false, optimisticUpdate: true }, - ), + const { result } = renderHook( + () => + useClientQueries(schema).user.useFindFirst( + { + include: { posts: true }, + }, + { optimisticUpdate: true }, + ), + { + wrapper, + }, ); - expect(cacheData.posts).toHaveLength(0); - }); - }); - - it('works with optimistic nested delete update query', async () => { - const { queryClient, wrapper } = createWrapper(); - - const data: any = [ - { id: 'p1', title: 'post1' }, - { id: 'p2', title: 'post2' }, - ]; - - nock(makeUrl('Post', 'findMany')) - .get(/.*/) - .reply(200, () => ({ data })) - .persist(); - - const { result } = renderHook( - () => useClientQueries(schema).post.useFindMany(undefined, { optimisticUpdate: true }), - { - wrapper, - }, - ); - await waitFor(() => { - expect(result.current.data).toHaveLength(2); - }); - - nock(makeUrl('User', 'update')) - .put(/.*/) - .reply(200, () => ({ data })); - - const { result: mutationResult } = renderHook( - () => - useClientQueries(schema).user.useUpdate({ - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - }, - ); - - act(() => mutationResult.current.mutate({ where: { id: '1' }, data: { posts: { delete: { id: 'p1' } } } })); - - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey('Post', 'findMany', undefined, { infinite: false, optimisticUpdate: true }), - ); - expect(cacheData).toHaveLength(1); - }); - }); - - it('top-level mutation and nested-read invalidation', async () => { - const { queryClient, wrapper } = createWrapper(); - - const queryArgs = { where: { id: '1' }, include: { posts: true } }; - const data = { posts: [{ id: '1', title: 'post1' }] }; - - nock(makeUrl('User', 'findUnique', queryArgs)) - .get(/.*/) - .reply(200, () => ({ data })) - .persist(); - - const { result } = renderHook(() => useClientQueries(schema).user.useFindUnique(queryArgs), { - wrapper, - }); - await waitFor(() => { - expect(result.current.data).toMatchObject(data); - }); - - nock(makeUrl('Post', 'update')) - .put(/.*/) - .reply(200, () => { - data.posts[0]!.title = 'post2'; - return data; + await waitFor(() => { + expect(result.current.data).toMatchObject({ id: '1' }); }); - const { result: mutationResult } = renderHook(() => useClientQueries(schema).post.useUpdate(), { - wrapper, + nock(makeUrl('Post', 'delete')) + .delete(/.*/) + .reply(200, () => ({ data })); + + const { result: mutationResult } = renderHook( + () => + useClientQueries(schema).post.useDelete({ + optimisticUpdate: true, + invalidateQueries: false, + }), + { + wrapper, + }, + ); + + act(() => mutationResult.current.mutate({ where: { id: 'p1' } })); + + await waitFor(() => { + const cacheData: any = queryClient.getQueryData( + getQueryKey( + 'User', + 'findFirst', + { include: { posts: true } }, + { infinite: false, optimisticUpdate: true }, + ), + ); + expect(cacheData.posts).toHaveLength(0); + }); }); - act(() => mutationResult.current.mutate({ where: { id: '1' }, data: { title: 'post2' } })); + it('works with optimistic nested delete update query', async () => { + const { queryClient, wrapper } = createWrapper(); - await waitFor(() => { - const cacheData: any = queryClient.getQueryData(getQueryKey('User', 'findUnique', queryArgs)); - expect(cacheData.posts[0].title).toBe('post2'); - }); - }); + const data: any = [ + { id: 'p1', title: 'post1' }, + { id: 'p2', title: 'post2' }, + ]; - it('nested mutation and top-level-read invalidation', async () => { - const { queryClient, wrapper } = createWrapper(); + nock(makeUrl('Post', 'findMany')) + .get(/.*/) + .reply(200, () => ({ data })) + .persist(); - const data = [{ id: '1', title: 'post1', ownerId: '1' }]; - - nock(makeUrl('Post', 'findMany')) - .get(/.*/) - .reply(200, () => ({ - data, - })) - .persist(); - - const { result } = renderHook(() => useClientQueries(schema).post.useFindMany(), { - wrapper, - }); - await waitFor(() => { - expect(result.current.data).toMatchObject(data); - }); - - nock(makeUrl('User', 'update')) - .put(/.*/) - .reply(200, () => { - data.push({ id: '2', title: 'post2', ownerId: '1' }); - return data; + const { result } = renderHook( + () => useClientQueries(schema).post.useFindMany(undefined, { optimisticUpdate: true }), + { + wrapper, + }, + ); + await waitFor(() => { + expect(result.current.data).toHaveLength(2); }); - const { result: mutationResult } = renderHook(() => useClientQueries(schema).user.useUpdate(), { - wrapper, - }); + nock(makeUrl('User', 'update')) + .put(/.*/) + .reply(200, () => ({ data })); - act(() => - mutationResult.current.mutate({ where: { id: '1' }, data: { posts: { create: { title: 'post2' } } } }), - ); - - await waitFor(() => { - const cacheData: any = queryClient.getQueryData(getQueryKey('Post', 'findMany', undefined)); - expect(cacheData).toHaveLength(2); - }); - }); - - it('optimistic create with custom provider', async () => { - const { queryClient, wrapper } = createWrapper(); - - const data: any[] = []; - - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => ({ data })) - .persist(); - - const { result } = renderHook( - () => useClientQueries(schema).user.useFindMany(undefined, { optimisticUpdate: true }), - { - wrapper, - }, - ); - await waitFor(() => { - expect(result.current.data).toHaveLength(0); - }); - - nock(makeUrl('User', 'create')) - .post(/.*/) - .reply(200, () => ({ data: null })) - .persist(); - - const { result: mutationResult1 } = renderHook( - () => - useClientQueries(schema).user.useCreate({ - optimisticUpdate: true, - invalidateQueries: false, - optimisticDataProvider: ({ queryModel, queryOperation }) => { - if (queryModel === 'User' && queryOperation === 'findMany') { - return { kind: 'Skip' }; - } else { - return { kind: 'ProceedDefault' }; - } - }, - }), - { - wrapper, - }, - ); - - act(() => mutationResult1.current.mutate({ data: { email: 'foo' } })); - - // cache should not update - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }), + const { result: mutationResult } = renderHook( + () => + useClientQueries(schema).user.useUpdate({ + optimisticUpdate: true, + invalidateQueries: false, + }), + { + wrapper, + }, ); - expect(cacheData).toHaveLength(0); + + act(() => mutationResult.current.mutate({ where: { id: '1' }, data: { posts: { delete: { id: 'p1' } } } })); + + await waitFor(() => { + const cacheData: any = queryClient.getQueryData( + getQueryKey('Post', 'findMany', undefined, { infinite: false, optimisticUpdate: true }), + ); + expect(cacheData).toHaveLength(1); + }); }); - const { result: mutationResult2 } = renderHook( - () => - useClientQueries(schema).user.useCreate({ - optimisticUpdate: true, - invalidateQueries: false, - optimisticDataProvider: ({ queryModel, queryOperation, currentData, mutationArgs }) => { - if (queryModel === 'User' && queryOperation === 'findMany') { - return { - kind: 'Update', - data: [ - ...currentData, - { id: 100, email: mutationArgs.data.email + 'hooray', $optimistic: true }, - ], - }; - } else { - return { kind: 'ProceedDefault' }; - } - }, - }), - { - wrapper, - }, - ); + it('optimistic create with custom provider', async () => { + const { queryClient, wrapper } = createWrapper(); - act(() => mutationResult2.current.mutate({ data: { email: 'foo' } })); + const data: any[] = []; - // cache should update - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }), + nock(makeUrl('User', 'findMany')) + .get(/.*/) + .reply(200, () => ({ data })) + .persist(); + + const { result } = renderHook( + () => useClientQueries(schema).user.useFindMany(undefined, { optimisticUpdate: true }), + { + wrapper, + }, ); - expect(cacheData).toHaveLength(1); - expect(cacheData[0].$optimistic).toBe(true); - expect(cacheData[0].id).toBeTruthy(); - expect(cacheData[0].email).toBe('foohooray'); - }); - }); - - it('optimistic update mixed with non-zenstack queries', async () => { - const { queryClient, wrapper } = createWrapper(); - - // non-zenstack query - const { result: myQueryResult } = renderHook( - () => useQuery({ queryKey: ['myQuery'], queryFn: () => ({ data: 'myData' }) }), - { - wrapper, - }, - ); - await waitFor(() => { - expect(myQueryResult.current.data).toEqual({ data: 'myData' }); - }); - - const data: any[] = []; - - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => ({ data })) - .persist(); - - const { result } = renderHook( - () => useClientQueries(schema).user.useFindMany(undefined, { optimisticUpdate: true }), - { - wrapper, - }, - ); - await waitFor(() => { - expect(result.current.data).toHaveLength(0); - }); - - nock(makeUrl('User', 'create')) - .post(/.*/) - .reply(200, () => ({ data: null })); - - const { result: mutationResult } = renderHook( - () => - useClientQueries(schema).user.useCreate({ - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - }, - ); - - act(() => mutationResult.current.mutate({ data: { email: 'foo' } })); - - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }), - ); - expect(cacheData).toHaveLength(1); - expect(cacheData[0].$optimistic).toBe(true); - expect(cacheData[0].id).toBeTruthy(); - expect(cacheData[0].email).toBe('foo'); - }); - }); - - it('works with sequential transaction and invalidation', async () => { - const { queryClient, wrapper } = createWrapper(); - - const users: any[] = []; - const posts: any[] = []; - - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => ({ data: users })) - .persist(); - - nock(makeUrl('Post', 'findMany')) - .get(/.*/) - .reply(200, () => ({ data: posts })) - .persist(); - - const { result: userResult } = renderHook(() => useClientQueries(schema).user.useFindMany(), { wrapper }); - const { result: postResult } = renderHook(() => useClientQueries(schema).post.useFindMany(), { wrapper }); - - await waitFor(() => { - expect(userResult.current.data).toHaveLength(0); - expect(postResult.current.data).toHaveLength(0); - }); - - nock(`${BASE_URL}/api/model/$transaction/sequential`) - .post(/.*/) - .reply(200, () => { - users.push({ id: '1', email: 'foo@bar.com' }); - posts.push({ id: 'p1', title: 'Hello' }); - return { data: [users[0], posts[0]] }; + await waitFor(() => { + expect(result.current.data).toHaveLength(0); }); - const { result: txResult } = renderHook(() => useClientQueries(schema).$transaction.useSequential(), { - wrapper, + nock(makeUrl('User', 'create')) + .post(/.*/) + .reply(200, () => ({ data: null })) + .persist(); + + const { result: mutationResult1 } = renderHook( + () => + useClientQueries(schema).user.useCreate({ + optimisticUpdate: true, + invalidateQueries: false, + optimisticDataProvider: ({ queryModel, queryOperation }) => { + if (queryModel === 'User' && queryOperation === 'findMany') { + return { kind: 'Skip' }; + } else { + return { kind: 'ProceedDefault' }; + } + }, + }), + { + wrapper, + }, + ); + + act(() => mutationResult1.current.mutate({ data: { email: 'foo' } })); + + // cache should not update + await waitFor(() => { + const cacheData: any = queryClient.getQueryData( + getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }), + ); + expect(cacheData).toHaveLength(0); + }); + + const { result: mutationResult2 } = renderHook( + () => + useClientQueries(schema).user.useCreate({ + optimisticUpdate: true, + invalidateQueries: false, + optimisticDataProvider: ({ queryModel, queryOperation, currentData, mutationArgs }) => { + if (queryModel === 'User' && queryOperation === 'findMany') { + return { + kind: 'Update', + data: [ + ...currentData, + { id: 100, email: mutationArgs.data.email + 'hooray', $optimistic: true }, + ], + }; + } else { + return { kind: 'ProceedDefault' }; + } + }, + }), + { + wrapper, + }, + ); + + act(() => mutationResult2.current.mutate({ data: { email: 'foo' } })); + + // cache should update + await waitFor(() => { + const cacheData: any = queryClient.getQueryData( + getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }), + ); + expect(cacheData).toHaveLength(1); + expect(cacheData[0].$optimistic).toBe(true); + expect(cacheData[0].id).toBeTruthy(); + expect(cacheData[0].email).toBe('foohooray'); + }); }); - act(() => - txResult.current.mutate([ - { model: 'User', op: 'create', args: { data: { email: 'foo@bar.com' } } }, - { model: 'Post', op: 'create', args: { data: { title: 'Hello' } } }, - ]), - ); + it('optimistic update mixed with non-zenstack queries', async () => { + const { queryClient, wrapper } = createWrapper(); - await waitFor(() => { - const cachedUsers = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined)); - const cachedPosts = queryClient.getQueryData(getQueryKey('Post', 'findMany', undefined)); - expect(cachedUsers).toHaveLength(1); - expect(cachedPosts).toHaveLength(1); + // non-zenstack query + const { result: myQueryResult } = renderHook( + () => useQuery({ queryKey: ['myQuery'], queryFn: () => ({ data: 'myData' }) }), + { + wrapper, + }, + ); + await waitFor(() => { + expect(myQueryResult.current.data).toEqual({ data: 'myData' }); + }); + + const data: any[] = []; + + nock(makeUrl('User', 'findMany')) + .get(/.*/) + .reply(200, () => ({ data })) + .persist(); + + const { result } = renderHook( + () => useClientQueries(schema).user.useFindMany(undefined, { optimisticUpdate: true }), + { + wrapper, + }, + ); + await waitFor(() => { + expect(result.current.data).toHaveLength(0); + }); + + nock(makeUrl('User', 'create')) + .post(/.*/) + .reply(200, () => ({ data: null })); + + const { result: mutationResult } = renderHook( + () => + useClientQueries(schema).user.useCreate({ + optimisticUpdate: true, + invalidateQueries: false, + }), + { + wrapper, + }, + ); + + act(() => mutationResult.current.mutate({ data: { email: 'foo' } })); + + await waitFor(() => { + const cacheData: any = queryClient.getQueryData( + getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }), + ); + expect(cacheData).toHaveLength(1); + expect(cacheData[0].$optimistic).toBe(true); + expect(cacheData[0].id).toBeTruthy(); + expect(cacheData[0].email).toBe('foo'); + }); }); }); - it('works with sequential transaction and no invalidation', async () => { - const { queryClient, wrapper } = createWrapper(); + describe('Sequential transaction', () => { + it('works with sequential transaction and invalidation', async () => { + const { queryClient, wrapper } = createWrapper(); - const users: any[] = []; + const users: any[] = []; + const posts: any[] = []; - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => ({ data: users })) - .persist(); + nock(makeUrl('User', 'findMany')) + .get(/.*/) + .reply(200, () => ({ data: users })) + .persist(); - const { result: userResult } = renderHook(() => useClientQueries(schema).user.useFindMany(), { wrapper }); + nock(makeUrl('Post', 'findMany')) + .get(/.*/) + .reply(200, () => ({ data: posts })) + .persist(); - await waitFor(() => { - expect(userResult.current.data).toHaveLength(0); - }); + const { result: userResult } = renderHook(() => useClientQueries(schema).user.useFindMany(), { wrapper }); + const { result: postResult } = renderHook(() => useClientQueries(schema).post.useFindMany(), { wrapper }); - nock(`${BASE_URL}/api/model/$transaction/sequential`) - .post(/.*/) - .reply(200, () => { - users.push({ id: '1', email: 'foo@bar.com' }); - return { data: [users[0]] }; + await waitFor(() => { + expect(userResult.current.data).toHaveLength(0); + expect(postResult.current.data).toHaveLength(0); }); - const { result: txResult } = renderHook( - () => useClientQueries(schema).$transaction.useSequential({ invalidateQueries: false }), - { wrapper }, - ); + nock(`${BASE_URL}/api/model/$transaction/sequential`) + .post(/.*/) + .reply(200, () => { + users.push({ id: '1', email: 'foo@bar.com' }); + posts.push({ id: 'p1', title: 'Hello' }); + return { data: [users[0], posts[0]] }; + }); - act(() => txResult.current.mutate([{ model: 'User', op: 'create', args: { data: { email: 'foo@bar.com' } } }])); + const { result: txResult } = renderHook(() => useClientQueries(schema).$transaction.useSequential(), { + wrapper, + }); - await waitFor(() => { - expect(txResult.current.isSuccess).toBe(true); - // cache not refreshed because invalidation was disabled - const cachedUsers = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined)); - expect(cachedUsers).toHaveLength(0); + act(() => + txResult.current.mutate([ + { model: 'User', op: 'create', args: { data: { email: 'foo@bar.com' } } }, + { model: 'Post', op: 'create', args: { data: { title: 'Hello' } } }, + ]), + ); + + await waitFor(() => { + const cachedUsers = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined)); + const cachedPosts = queryClient.getQueryData(getQueryKey('Post', 'findMany', undefined)); + expect(cachedUsers).toHaveLength(1); + expect(cachedPosts).toHaveLength(1); + }); + }); + + it('works with sequential transaction and no invalidation', async () => { + const { queryClient, wrapper } = createWrapper(); + + const users: any[] = []; + + nock(makeUrl('User', 'findMany')) + .get(/.*/) + .reply(200, () => ({ data: users })) + .persist(); + + const { result: userResult } = renderHook(() => useClientQueries(schema).user.useFindMany(), { wrapper }); + + await waitFor(() => { + expect(userResult.current.data).toHaveLength(0); + }); + + nock(`${BASE_URL}/api/model/$transaction/sequential`) + .post(/.*/) + .reply(200, () => { + users.push({ id: '1', email: 'foo@bar.com' }); + return { data: [users[0]] }; + }); + + const { result: txResult } = renderHook( + () => useClientQueries(schema).$transaction.useSequential({ invalidateQueries: false }), + { wrapper }, + ); + + act(() => + txResult.current.mutate([{ model: 'User', op: 'create', args: { data: { email: 'foo@bar.com' } } }]), + ); + + await waitFor(() => { + expect(txResult.current.isSuccess).toBe(true); + // cache not refreshed because invalidation was disabled + const cachedUsers = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined)); + expect(cachedUsers).toHaveLength(0); + }); }); });