diff --git a/src/fetch/index.ts b/src/fetch/index.ts index 93def88..a58de68 100644 --- a/src/fetch/index.ts +++ b/src/fetch/index.ts @@ -51,8 +51,7 @@ const handleResponse = async (response: Response, retry: () => any) => { } } -export const edenFetch = - >( +export const edenFetch = >( server: string, config?: EdenFetch.Config ): EdenFetch.Create => @@ -72,9 +71,19 @@ export const edenFetch = } catch (error) {} const fetch = config?.fetcher || globalThis.fetch - const queryStr = query - ? `?${new URLSearchParams(query).toString()}` - : '' + + const nonNullishQuery = query + ? Object.fromEntries( + Object.entries(query).filter( + ([_, val]) => val !== undefined && val !== null + ) + ) + : null + + const queryStr = nonNullishQuery + ? `?${new URLSearchParams(nonNullishQuery).toString()}` + : '' + const requestUrl = `${server}${endpoint}${queryStr}` const headers = body ? { diff --git a/src/treaty2/index.ts b/src/treaty2/index.ts index 1e494d0..deb4540 100644 --- a/src/treaty2/index.ts +++ b/src/treaty2/index.ts @@ -196,12 +196,17 @@ const createProxy = ( continue } - if (typeof value === 'object') { - append(key, JSON.stringify(value)) + // Explicitly exclude null and undefined values from url encoding + // to prevent parsing string "null" / string "undefined" + if (value === undefined || value === null) { continue } + if (typeof value === 'object') { + append(key, JSON.stringify(value)) + continue + } append(key, `${value}`) } } diff --git a/test/fetch.test.ts b/test/fetch.test.ts index d9a5c51..15f7d32 100644 --- a/test/fetch.test.ts +++ b/test/fetch.test.ts @@ -68,6 +68,32 @@ const app = new Elysia() }) } ) + .get( + '/with-query-undefined', + ({ query }) => { + return { + query + } + }, + { + query: t.Object({ + q: t.Undefined(t.String()) + }) + } + ) + .get( + '/with-query-nullish', + ({ query }) => { + return { + query + } + }, + { + query: t.Object({ + q: t.Nullable(t.String()) + }) + } + ) .listen(8081) const fetch = edenFetch('http://localhost:8081') @@ -170,4 +196,27 @@ describe('Eden Fetch', () => { }) expect(data?.query.q).toBe('A') }) + + it('send undefined query', async () => { + const { data, error } = await fetch('/with-query-undefined', { + query: { + q: undefined + } + }) + expect(data?.query.q).toBeUndefined() + expect(error).toBeNull() + }) + + // t.Nullable is impossible to represent with query params + // without elysia specifically parsing 'null' + it('send null query', async () => { + const { data, error } = await fetch('/with-query-nullish', { + query: { + q: null + } + }) + expect(data?.query.q).toBeUndefined() + expect(error?.status).toBe(422) + expect(error?.value.type).toBe("validation") + }) }) diff --git a/test/treaty2.test.ts b/test/treaty2.test.ts index d12a9ca..a023cf7 100644 --- a/test/treaty2.test.ts +++ b/test/treaty2.test.ts @@ -64,12 +64,34 @@ const app = new Elysia() username: t.String() }) }) + .get('/query-optional', ({ query }) => query, { + query: t.Object({ + username: t.Optional(t.String()), + }) + }) + .get('/query-nullable', ({ query }) => query, { + query: t.Object({ + username: t.Nullable(t.String()), + }) + }) .get('/queries', ({ query }) => query, { query: t.Object({ username: t.String(), alias: t.Literal('Kristen') }) }) + .get('/queries-optional', ({ query }) => query, { + query: t.Object({ + username: t.Optional(t.String()), + alias: t.Literal('Kristen') + }) + }) + .get('/queries-nullable', ({ query }) => query, { + query: t.Object({ + username: t.Nullable(t.Number()), + alias: t.Literal('Kristen') + }) + }) .post('/queries', ({ query }) => query, { query: t.Object({ username: t.String(), @@ -273,6 +295,32 @@ describe('Treaty2', () => { expect(data).toEqual(query) }) + // t.Nullable is impossible to represent with query params + // without elysia specifically parsing 'null' + it('get null query', async () => { + const query = { username: null } + + const { data, error } = await client['query-nullable'].get({ + query + }) + + expect(data).toBeNull() + expect(error?.status).toBe(422) + expect(error?.value.type).toBe("validation") + }) + + it('get optional query', async () => { + const query = { username: undefined } + + const { data } = await client['query-optional'].get({ + query + }) + + expect(data).toEqual({ + username: undefined + }) + }) + it('get queries', async () => { const query = { username: 'A', alias: 'Kristen' } as const @@ -283,6 +331,33 @@ describe('Treaty2', () => { expect(data).toEqual(query) }) + it('get optional queries', async () => { + const query = { username: undefined, alias: 'Kristen' } as const + + const { data } = await client['queries-optional'].get({ + query + }) + + expect(data).toEqual({ + username: undefined, + alias: 'Kristen' + }) + }) + + // t.Nullable is impossible to represent with query params + // without elysia specifically parsing 'null' + it('get nullable queries', async () => { + const query = { username: null, alias: 'Kristen' } as const + + const { data, error } = await client['queries-nullable'].get({ + query + }) + + expect(data).toBeNull() + expect(error?.status).toBe(422) + expect(error?.value.type).toBe("validation") + }) + it('post queries', async () => { const query = { username: 'A', alias: 'Kristen' } as const