44
55import { QueryClient , QueryClientProvider , useQuery } from '@tanstack/react-query' ;
66import { act , cleanup , renderHook , waitFor } from '@testing-library/react' ;
7+ import { deserialize , serialize } from '@zenstackhq/client-helpers/fetch' ;
78import nock from 'nock' ;
89import React from 'react' ;
910import { afterEach , describe , expect , it } from 'vitest' ;
1011import { getQueryKey } from '../src/common/query-key' ;
11- import { QuerySettingsProvider , useClientQueries } from '../src/react' ;
12+ import { AnyNull , DbNull , JsonNull , QuerySettingsProvider , useClientQueries } from '../src/react' ;
1213import { schema } from './schemas/basic/schema-lite' ;
1314
1415const BASE_URL = 'http://localhost' ;
@@ -1817,10 +1818,9 @@ describe('React Query Test', () => {
18171818 return { data : [ users [ 0 ] , posts [ 0 ] ] } ;
18181819 } ) ;
18191820
1820- const { result : txResult } = renderHook (
1821- ( ) => useClientQueries ( schema ) . $transaction . useSequential ( ) ,
1822- { wrapper } ,
1823- ) ;
1821+ const { result : txResult } = renderHook ( ( ) => useClientQueries ( schema ) . $transaction . useSequential ( ) , {
1822+ wrapper,
1823+ } ) ;
18241824
18251825 act ( ( ) =>
18261826 txResult . current . mutate ( [
@@ -1865,9 +1865,7 @@ describe('React Query Test', () => {
18651865 { wrapper } ,
18661866 ) ;
18671867
1868- act ( ( ) =>
1869- txResult . current . mutate ( [ { model : 'User' , op : 'create' , args : { data : { email : 'foo@bar.com' } } } ] ) ,
1870- ) ;
1868+ act ( ( ) => txResult . current . mutate ( [ { model : 'User' , op : 'create' , args : { data : { email : 'foo@bar.com' } } } ] ) ) ;
18711869
18721870 await waitFor ( ( ) => {
18731871 expect ( txResult . current . isSuccess ) . toBe ( true ) ;
@@ -1876,4 +1874,140 @@ describe('React Query Test', () => {
18761874 expect ( cachedUsers ) . toHaveLength ( 0 ) ;
18771875 } ) ;
18781876 } ) ;
1877+
1878+ describe ( 'JSON null value serialization' , ( ) => {
1879+ function createWrapper ( ) {
1880+ const queryClient = new QueryClient ( { defaultOptions : { queries : { retry : false } } } ) ;
1881+ const wrapper = ( { children } : { children : React . ReactNode } ) => (
1882+ < QueryClientProvider client = { queryClient } >
1883+ < QuerySettingsProvider value = { { endpoint : `${ BASE_URL } /api/model` } } >
1884+ { children }
1885+ </ QuerySettingsProvider >
1886+ </ QueryClientProvider >
1887+ ) ;
1888+ return { queryClient, wrapper } ;
1889+ }
1890+
1891+ it ( 'encodes DbNull in query filter and includes serialization metadata in URL' , async ( ) => {
1892+ const { wrapper } = createWrapper ( ) ;
1893+ let capturedUri = '' ;
1894+
1895+ nock ( BASE_URL )
1896+ . get ( / .* / )
1897+ . reply ( 200 , function ( uri ) {
1898+ capturedUri = uri ;
1899+ return { data : [ ] } ;
1900+ } ) ;
1901+
1902+ const { result } = renderHook (
1903+ ( ) => useClientQueries ( schema ) . user . useFindMany ( { where : { name : DbNull } } as any ) ,
1904+ { wrapper } ,
1905+ ) ;
1906+
1907+ await waitFor ( ( ) => expect ( result . current . isSuccess ) . toBe ( true ) ) ;
1908+
1909+ const url = new URL ( capturedUri , BASE_URL ) ;
1910+ expect ( url . searchParams . has ( 'meta' ) ) . toBe ( true ) ;
1911+
1912+ const q = JSON . parse ( decodeURIComponent ( url . searchParams . get ( 'q' ) ! ) ) ;
1913+ const meta = JSON . parse ( decodeURIComponent ( url . searchParams . get ( 'meta' ) ! ) ) ;
1914+ const reconstructed = deserialize ( q , meta . serialization ) as any ;
1915+ expect ( reconstructed . where . name . __brand ) . toBe ( 'DbNull' ) ;
1916+ } ) ;
1917+
1918+ it ( 'encodes JsonNull in query filter and includes serialization metadata in URL' , async ( ) => {
1919+ const { wrapper } = createWrapper ( ) ;
1920+ let capturedUri = '' ;
1921+
1922+ nock ( BASE_URL )
1923+ . get ( / .* / )
1924+ . reply ( 200 , function ( uri ) {
1925+ capturedUri = uri ;
1926+ return { data : [ ] } ;
1927+ } ) ;
1928+
1929+ const { result } = renderHook (
1930+ ( ) => useClientQueries ( schema ) . user . useFindMany ( { where : { name : JsonNull } } as any ) ,
1931+ { wrapper } ,
1932+ ) ;
1933+
1934+ await waitFor ( ( ) => expect ( result . current . isSuccess ) . toBe ( true ) ) ;
1935+
1936+ const url = new URL ( capturedUri , BASE_URL ) ;
1937+ expect ( url . searchParams . has ( 'meta' ) ) . toBe ( true ) ;
1938+
1939+ const q = JSON . parse ( decodeURIComponent ( url . searchParams . get ( 'q' ) ! ) ) ;
1940+ const meta = JSON . parse ( decodeURIComponent ( url . searchParams . get ( 'meta' ) ! ) ) ;
1941+ const reconstructed = deserialize ( q , meta . serialization ) as any ;
1942+ expect ( reconstructed . where . name . __brand ) . toBe ( 'JsonNull' ) ;
1943+ } ) ;
1944+
1945+ it ( 'encodes AnyNull in query filter and includes serialization metadata in URL' , async ( ) => {
1946+ const { wrapper } = createWrapper ( ) ;
1947+ let capturedUri = '' ;
1948+
1949+ nock ( BASE_URL )
1950+ . get ( / .* / )
1951+ . reply ( 200 , function ( uri ) {
1952+ capturedUri = uri ;
1953+ return { data : [ ] } ;
1954+ } ) ;
1955+
1956+ const { result } = renderHook (
1957+ ( ) => useClientQueries ( schema ) . user . useFindMany ( { where : { name : AnyNull } } as any ) ,
1958+ { wrapper } ,
1959+ ) ;
1960+
1961+ await waitFor ( ( ) => expect ( result . current . isSuccess ) . toBe ( true ) ) ;
1962+
1963+ const url = new URL ( capturedUri , BASE_URL ) ;
1964+ expect ( url . searchParams . has ( 'meta' ) ) . toBe ( true ) ;
1965+
1966+ const q = JSON . parse ( decodeURIComponent ( url . searchParams . get ( 'q' ) ! ) ) ;
1967+ const meta = JSON . parse ( decodeURIComponent ( url . searchParams . get ( 'meta' ) ! ) ) ;
1968+ const reconstructed = deserialize ( q , meta . serialization ) as any ;
1969+ expect ( reconstructed . where . name . __brand ) . toBe ( 'AnyNull' ) ;
1970+ } ) ;
1971+
1972+ it ( 'encodes DbNull in mutation body with serialization metadata' , async ( ) => {
1973+ const { wrapper } = createWrapper ( ) ;
1974+ let capturedBody : any ;
1975+
1976+ nock ( BASE_URL )
1977+ . post ( / .* / )
1978+ . reply ( 200 , function ( _uri , body ) {
1979+ capturedBody = body ;
1980+ return { data : { id : '1' , name : null } } ;
1981+ } ) ;
1982+
1983+ const { result } = renderHook ( ( ) => useClientQueries ( schema ) . user . useCreate ( ) , { wrapper } ) ;
1984+
1985+ act ( ( ) => result . current . mutate ( { data : { email : 'test@example.com' , name : DbNull } } as any ) ) ;
1986+
1987+ await waitFor ( ( ) => expect ( result . current . isSuccess ) . toBe ( true ) ) ;
1988+
1989+ expect ( capturedBody . meta ?. serialization ) . toBeDefined ( ) ;
1990+ const reconstructed = deserialize ( { data : capturedBody . data } , capturedBody . meta . serialization ) as any ;
1991+ expect ( reconstructed . data . name . __brand ) . toBe ( 'DbNull' ) ;
1992+ } ) ;
1993+
1994+ it ( 'deserializes null sentinels in server response back to branded instances' , async ( ) => {
1995+ const { wrapper } = createWrapper ( ) ;
1996+
1997+ const responseData = { id : '1' , email : 'test@example.com' , name : DbNull } ;
1998+ const { data : serializedData , meta : serializedMeta } = serialize ( responseData ) ;
1999+
2000+ nock ( BASE_URL )
2001+ . get ( / .* / )
2002+ . reply ( 200 , { data : serializedData , meta : { serialization : serializedMeta } } ) ;
2003+
2004+ const { result } = renderHook ( ( ) => useClientQueries ( schema ) . user . useFindUnique ( { where : { id : '1' } } ) , {
2005+ wrapper,
2006+ } ) ;
2007+
2008+ await waitFor ( ( ) => expect ( result . current . isSuccess ) . toBe ( true ) ) ;
2009+
2010+ expect ( ( result . current . data as any ) . name . __brand ) . toBe ( 'DbNull' ) ;
2011+ } ) ;
2012+ } ) ;
18792013} ) ;
0 commit comments