Skip to content

Commit 57acbad

Browse files
committed
feat: Add optional secondary param if query args arne't required
1 parent 0f79ecc commit 57acbad

12 files changed

Lines changed: 532 additions & 454 deletions

File tree

.changeset/fresh-cups-speak.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@ts-rest/core': minor
3+
'@ts-rest/react-query': minor
4+
'@ts-rest/solid-query': minor
5+
---
6+
7+
Added the ability to omit the second parameter if there no required query parameters

apps/example-microservice/web-app/src/App.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@ import React from 'react';
22
import { postsClient } from './main';
33

44
export const App = () => {
5-
const { data } = postsClient.getPosts.useQuery(['posts'], {
6-
query: {},
7-
});
5+
const { data } = postsClient.getPosts.useQuery(['posts']);
86

97
const posts = data?.body || [];
108

libs/ts-rest/react-query/src/lib/ts-rest-client.spec.tsx renamed to apps/example-next/tests/react-query.spec.tsx

Lines changed: 110 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,115 @@
1-
import { router } from './test-fixtures';
2-
import { initQueryClient } from './ts-rest-client';
3-
import { renderHook } from '@testing-library/react-hooks';
4-
import { waitFor } from '@testing-library/react';
51
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
6-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
7-
// @ts-ignore
2+
import { waitFor } from '@testing-library/react';
3+
import { renderHook } from '@testing-library/react-hooks';
4+
import { initContract } from '@ts-rest/core';
5+
import { initQueryClient } from '@ts-rest/react-query';
86
import React from 'react';
7+
import { z } from 'zod';
8+
9+
const c = initContract();
10+
11+
export type Post = {
12+
id: string;
13+
title: string;
14+
description: string | null;
15+
content: string | null;
16+
published: boolean;
17+
authorId: string;
18+
};
19+
20+
export type User = {
21+
id: string;
22+
email: string;
23+
name: string | null;
24+
};
25+
26+
const postsRouter = c.router({
27+
getPost: {
28+
method: 'GET',
29+
path: `/posts/:id`,
30+
responses: {
31+
200: c.response<Post | null>(),
32+
},
33+
},
34+
getPosts: {
35+
method: 'GET',
36+
path: '/posts',
37+
responses: {
38+
200: c.response<Post[]>(),
39+
},
40+
query: z.object({
41+
take: z.number().optional(),
42+
skip: z.number().optional(),
43+
}),
44+
},
45+
createPost: {
46+
method: 'POST',
47+
path: '/posts',
48+
responses: {
49+
200: c.response<Post>(),
50+
},
51+
body: z.object({
52+
title: z.string(),
53+
content: z.string(),
54+
published: z.boolean().optional(),
55+
description: z.string().optional(),
56+
authorId: z.string(),
57+
}),
58+
},
59+
mutationWithQuery: {
60+
method: 'POST',
61+
path: '/posts',
62+
responses: {
63+
200: c.response<Post>(),
64+
},
65+
body: z.object({}),
66+
query: z.object({
67+
test: z.string(),
68+
}),
69+
},
70+
updatePost: {
71+
method: 'PUT',
72+
path: `/posts/:id`,
73+
responses: {
74+
200: c.response<Post>(),
75+
},
76+
body: z.object({
77+
title: z.string(),
78+
content: z.string(),
79+
published: z.boolean().optional(),
80+
description: z.string().optional(),
81+
authorId: z.string(),
82+
}),
83+
},
84+
patchPost: {
85+
method: 'PATCH',
86+
path: `/posts/:id`,
87+
responses: {
88+
200: c.response<Post>(),
89+
},
90+
body: null,
91+
},
92+
deletePost: {
93+
method: 'DELETE',
94+
path: `/posts/:id`,
95+
responses: {
96+
200: c.response<boolean>(),
97+
},
98+
body: null,
99+
},
100+
});
101+
102+
// Three endpoints, two for posts, and one for health
103+
export const router = c.router({
104+
posts: postsRouter,
105+
health: {
106+
method: 'GET',
107+
path: '/health',
108+
responses: {
109+
200: c.response<{ message: string }>(),
110+
},
111+
},
112+
});
9113

10114
const api = jest.fn();
11115

libs/ts-rest/core/src/lib/client.spec.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { initClient } from './client';
21
import { initContract } from '..';
2+
import { initClient } from './client';
33

44
import { z } from 'zod';
55

@@ -149,6 +149,24 @@ describe('client', () => {
149149
});
150150
});
151151

152+
it('w/ no parameters (not provided)', async () => {
153+
const value = { key: 'value' };
154+
api.mockResolvedValue({ body: value, status: 200 });
155+
156+
const result = await client.posts.getPosts();
157+
158+
expect(result).toStrictEqual({ body: value, status: 200 });
159+
160+
expect(api).toHaveBeenCalledWith({
161+
method: 'GET',
162+
path: 'http://api.com/posts',
163+
headers: {
164+
'Content-Type': 'application/json',
165+
},
166+
body: undefined,
167+
});
168+
});
169+
152170
it('w/ query parameters', async () => {
153171
const value = { key: 'value' };
154172
api.mockResolvedValue({ body: value, status: 200 });

libs/ts-rest/core/src/lib/client.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@ import { AppRoute, AppRouteMutation, AppRouter, isAppRoute } from './dsl';
33
import { insertParamsIntoPath, ParamsFromUrl } from './paths';
44
import { convertQueryParamsToUrlString } from './query';
55
import { HTTPStatusCode } from './status-codes';
6-
import { Merge, Without, ZodInferOrType } from './type-utils';
6+
import {
7+
AreAllPropertiesOptional,
8+
Merge,
9+
Without,
10+
ZodInferOrType,
11+
} from './type-utils';
712

813
type RecursiveProxyObj<T extends AppRouter> = {
914
[TKey in keyof T]: T[TKey] extends AppRoute
10-
? DataReturn<T[TKey]>
15+
? AppRouteFunction<T[TKey]>
1116
: T[TKey] extends AppRouter
1217
? RecursiveProxyObj<T[TKey]>
1318
: never;
@@ -43,7 +48,9 @@ interface DataReturnArgs<TRoute extends AppRoute> {
4348
? AppRouteBodyOrFormData<TRoute>
4449
: never;
4550
params: PathParamsFromUrl<TRoute>;
46-
query: TRoute['query'] extends ZodTypeAny
51+
query: AreAllPropertiesOptional<
52+
AppRouteMutationType<TRoute['query']>
53+
> extends false
4754
? AppRouteMutationType<TRoute['query']>
4855
: never;
4956
}
@@ -63,9 +70,14 @@ export type ApiRouteResponse<T> =
6370
/**
6471
* Returned from a mutation or query call
6572
*/
66-
export type DataReturn<TRoute extends AppRoute> = (
67-
args: Without<DataReturnArgs<TRoute>, never>
68-
) => Promise<ApiRouteResponse<TRoute['responses']>>;
73+
export type AppRouteFunction<TRoute extends AppRoute> =
74+
AreAllPropertiesOptional<Without<DataReturnArgs<TRoute>, never>> extends true
75+
? (
76+
args?: Without<DataReturnArgs<TRoute>, never>
77+
) => Promise<ApiRouteResponse<TRoute['responses']>>
78+
: (
79+
args: Without<DataReturnArgs<TRoute>, never>
80+
) => Promise<ApiRouteResponse<TRoute['responses']>>;
6981

7082
export interface ClientArgs {
7183
baseUrl: string;

libs/ts-rest/core/src/lib/type-utils.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,9 @@ type NarrowRaw<T> =
8888
type NarrowNotZod<T> = Try<T, ZodType, NarrowRaw<T>>;
8989

9090
export type Narrow<T> = Try<T, [], NarrowNotZod<T>>;
91+
92+
export type AreAllPropertiesOptional<T> = {
93+
[K in keyof T]-?: undefined extends T[K] ? true : false;
94+
}[keyof T] extends true
95+
? true
96+
: false;

libs/ts-rest/react-query/src/lib/test-fixtures.ts

Lines changed: 0 additions & 107 deletions
This file was deleted.

0 commit comments

Comments
 (0)