Skip to content

Commit 7947f89

Browse files
committed
fix: make payload and body implicitly required
1 parent c627ad5 commit 7947f89

File tree

6 files changed

+85
-24
lines changed

6 files changed

+85
-24
lines changed

lib/build/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ export function generateClientType(server: Server) {
9999
const payloadTypeName = getTypeName(routeName, "payload");
100100
routeOptions.payload = {
101101
name: payloadTypeName,
102-
required: isRequired(route.settings.validate.payload as ExtendedSchema),
102+
required: true,
103103
};
104104
statements.push(typeAliasDeclaration(payloadTypeName, generateType(route.settings.validate.payload as ExtendedObjectSchema), true));
105105
}
@@ -120,7 +120,7 @@ export function generateClientType(server: Server) {
120120
ok: { name: (+code >= 200 && +code < 300) ? "true" : "false", required: true },
121121
headers: { name: "Headers", required: true },
122122
url: { name: "string", required: true },
123-
body: { node: generateType(schema as ExtendedObjectSchema), required: isRequired(schema as ExtendedSchema) },
123+
body: { node: generateType(schema as ExtendedObjectSchema), required: true },
124124
});
125125
statements.push(typeAliasDeclaration(responseCodeTypeName, responseNode, true));
126126
responseTypeList.push(responseCodeTypeName);

lib/index.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ import type { Server } from "@hapi/hapi";
22
import type {
33
AvailablePaths,
44
FetchOptions,
5-
HasRequiredFields,
65
MatchingRoute,
76
RequestMethod,
87
ResponseType,
98
BodyType,
109
Route,
1110
Simplify,
1211
StatusCode,
12+
OptionsArgument,
1313
} from "./types.ts";
1414

1515
export type * from "./types.ts";
@@ -21,7 +21,7 @@ export type ClientOptions = Simplify<
2121
>;
2222

2323
export class Client<T extends Route> {
24-
#url: URL;
24+
#url: URL | undefined;
2525
#server: Server | undefined;
2626
#headers: Headers;
2727

@@ -121,47 +121,47 @@ export class Client<T extends Route> {
121121
P extends AvailablePaths<T, "DELETE">,
122122
R extends MatchingRoute<T, "DELETE", P>,
123123
O extends FetchOptions<R>,
124-
>(path: P, ...args: (HasRequiredFields<O> extends true ? [O] : [O?])): Promise<ResponseType<R>> {
124+
>(path: P, ...args: OptionsArgument<O>): Promise<ResponseType<R>> {
125125
return await this.#fetch("DELETE", path, args[0]);
126126
}
127127

128128
async get<
129129
P extends AvailablePaths<T, "GET">,
130130
R extends MatchingRoute<T, "GET", P>,
131131
O extends FetchOptions<R>,
132-
>(path: P, ...args: (HasRequiredFields<O> extends true ? [O] : [O?])): Promise<ResponseType<R>> {
132+
>(path: P, ...args: OptionsArgument<O>): Promise<ResponseType<R>> {
133133
return await this.#fetch("GET", path, args[0]);
134134
}
135135

136136
async options<
137137
P extends AvailablePaths<T, "OPTIONS">,
138138
R extends MatchingRoute<T, "OPTIONS", P>,
139139
O extends FetchOptions<R>,
140-
>(path: P, ...args: (HasRequiredFields<O> extends true ? [O] : [O?])): Promise<ResponseType<R>> {
140+
>(path: P, ...args: OptionsArgument<O>): Promise<ResponseType<R>> {
141141
return await this.#fetch("OPTIONS", path, args[0]);
142142
}
143143

144144
async patch<
145145
P extends AvailablePaths<T, "PATCH">,
146146
R extends MatchingRoute<T, "PATCH", P>,
147147
O extends FetchOptions<R>,
148-
>(path: P, ...args: (HasRequiredFields<O> extends true ? [O] : [O?])): Promise<ResponseType<R>> {
148+
>(path: P, ...args: OptionsArgument<O>): Promise<ResponseType<R>> {
149149
return await this.#fetch("PATCH", path, args[0]);
150150
}
151151

152152
async post<
153153
P extends AvailablePaths<T, "POST">,
154154
R extends MatchingRoute<T, "POST", P>,
155155
O extends FetchOptions<R>,
156-
>(path: P, ...args: (HasRequiredFields<O> extends true ? [O] : [O?])): Promise<ResponseType<R>> {
156+
>(path: P, ...args: OptionsArgument<O>): Promise<ResponseType<R>> {
157157
return await this.#fetch("POST", path, args[0]);
158158
}
159159

160160
async put<
161161
P extends AvailablePaths<T, "PUT">,
162162
R extends MatchingRoute<T, "PUT", P>,
163163
O extends FetchOptions<R>,
164-
>(path: P, ...args: (HasRequiredFields<O> extends true ? [O] : [O?])): Promise<ResponseType<R>> {
164+
>(path: P, ...args: OptionsArgument<O>): Promise<ResponseType<R>> {
165165
return await this.#fetch("PUT", path, args[0]);
166166
}
167167
}

lib/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ export type FetchOptions<R extends Route> = Simplify<Omit<R["settings"], "respon
9898
next?: RequestInit["next"];
9999
}>;
100100

101+
export type OptionsArgument<O> = HasRequiredFields<O> extends true
102+
? [options: O]
103+
: [options?: O];
104+
101105
/**
102106
* Get the params type for a route
103107
*/

test/fetch.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ await test("fetch()", async (t) => {
3838
t.assert.partialDeepStrictEqual(firstCall.arguments, [{ next: { revalidate: 10 } }]);
3939
});
4040

41-
await t.test("/simple", async (t: TestContext) => {
41+
await t.test("GET /simple", async (t: TestContext) => {
4242
type routeType = MatchingRoute<Routes, "GET", "/simple">;
4343

4444
const res = await client.get("/simple");
@@ -47,7 +47,7 @@ await test("fetch()", async (t) => {
4747
t.assert.deepStrictEqual<SpecificBodyType<routeType, 200>>(res.body, { success: true });
4848
});
4949

50-
await t.test("/query", async (t: TestContext) => {
50+
await t.test("GET /query", async (t: TestContext) => {
5151
type routeType = MatchingRoute<Routes, "GET", "/query">;
5252

5353
const withoutQueryRes = await client.get("/query");
@@ -62,7 +62,7 @@ await test("fetch()", async (t) => {
6262
t.assert.deepStrictEqual<SpecificBodyType<routeType, 200>>(withQueryRes.body, { flag: true });
6363
});
6464

65-
await t.test("/param/{param}", async (t: TestContext) => {
65+
await t.test("GET /param/{param}", async (t: TestContext) => {
6666
type routeType = MatchingRoute<Routes, "GET", "/param/{param}">;
6767

6868
const passingRes = await client.get("/param/{param}", { params: { param: "pass" } });
@@ -75,4 +75,23 @@ await test("fetch()", async (t) => {
7575
t.assert.equal(failingRes.url, `${server.info.uri}/param/fail`);
7676
t.assert.deepStrictEqual<SpecificBodyType<routeType, 400>>(failingRes.body, { success: false, message: "failed" });
7777
});
78+
79+
await t.test("POST /post", async (t: TestContext) => {
80+
type routeType = MatchingRoute<Routes, "POST", "/post">;
81+
82+
const passingRes = await client.post("/post", { payload: { flag: true } });
83+
t.assert.equal(passingRes.status, 200);
84+
t.assert.equal(passingRes.url, `${server.info.uri}/post`);
85+
t.assert.deepStrictEqual<SpecificBodyType<routeType, 200>>(passingRes.body, { flag: true });
86+
87+
// @ts-expect-error Property 'flag' is missing in type '{}' but required in type PostPostPayload
88+
const failingRes = await client.post("/post", { payload: {} });
89+
t.assert.equal(failingRes.status, 400);
90+
t.assert.equal(failingRes.url, `${server.info.uri}/post`);
91+
t.assert.deepStrictEqual<SpecificBodyType<routeType, 400>>(failingRes.body, {
92+
error: "Bad Request",
93+
message: "Invalid request payload input",
94+
statusCode: 400,
95+
});
96+
});
7897
});

test/fixture.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ server.route([
1414
options: {
1515
response: {
1616
status: {
17-
200: Joi.object({ success: Joi.boolean().required() }).required(),
17+
200: Joi.object({ success: Joi.boolean().required() }),
1818
},
1919
},
2020
handler() {
@@ -33,7 +33,7 @@ server.route([
3333
},
3434
response: {
3535
status: {
36-
200: Joi.object({ flag: Joi.boolean().required() }).required(),
36+
200: Joi.object({ flag: Joi.boolean().required() }),
3737
},
3838
},
3939
handler(request: Request<{ Query: { flag: boolean } }>) {
@@ -47,13 +47,13 @@ server.route([
4747
options: {
4848
validate: {
4949
params: Joi.object({
50-
param: Joi.string().required(),
51-
}).required(),
50+
param: Joi.string(),
51+
}),
5252
},
5353
response: {
5454
status: {
55-
200: Joi.object({ success: Joi.boolean().valid(true) }).required(),
56-
400: Joi.object({ success: Joi.boolean().valid(false), message: Joi.string().required() }).required(),
55+
200: Joi.object({ success: Joi.boolean().valid(true).required() }),
56+
400: Joi.object({ success: Joi.boolean().valid(false).required(), message: Joi.string().required() }),
5757
},
5858
},
5959
handler(request: Request<{ Params: { param: string } }>, h) {
@@ -65,6 +65,25 @@ server.route([
6565
},
6666
},
6767
},
68+
{
69+
method: "POST",
70+
path: "/post",
71+
options: {
72+
validate: {
73+
payload: Joi.object({
74+
flag: Joi.boolean().required(),
75+
}),
76+
},
77+
response: {
78+
status: {
79+
200: Joi.object({ flag: Joi.boolean().required() }),
80+
},
81+
},
82+
handler(request: Request<{ Payload: { flag: boolean } }>) {
83+
return { flag: request.payload.flag };
84+
},
85+
},
86+
},
6887
]);
6988

7089
// TODO: test the types in here somehow

test/inject.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import { server, Client } from "./fixture.ts";
33
import type { MatchingRoute, QueryType, SpecificBodyType } from "@code4rena/typed-client";
44
import type { Routes } from "./generated.ts";
55

6-
await test("GET - server.inject", async (t) => {
7-
const client = new Client({ server });
6+
const client = new Client({ server });
87

9-
await t.test("/simple", async (t: TestContext) => {
8+
await test("server.inject()", async (t) => {
9+
await t.test("GET /simple", async (t: TestContext) => {
1010
type routeType = MatchingRoute<Routes, "GET", "/simple">;
1111

1212
const res = await client.get("/simple");
@@ -15,7 +15,7 @@ await test("GET - server.inject", async (t) => {
1515
t.assert.deepStrictEqual<SpecificBodyType<routeType, 200>>(res.body, { success: true });
1616
});
1717

18-
await t.test("/query", async (t: TestContext) => {
18+
await t.test("GET /query", async (t: TestContext) => {
1919
type routeType = MatchingRoute<Routes, "GET", "/query">;
2020

2121
const withoutQueryRes = await client.get("/query");
@@ -30,7 +30,7 @@ await test("GET - server.inject", async (t) => {
3030
t.assert.deepStrictEqual<SpecificBodyType<routeType, 200>>(withQueryRes.body, { flag: true });
3131
});
3232

33-
await t.test("/param/{param}", async (t: TestContext) => {
33+
await t.test("GET /param/{param}", async (t: TestContext) => {
3434
type routeType = MatchingRoute<Routes, "GET", "/param/{param}">;
3535

3636
const passingRes = await client.get("/param/{param}", { params: { param: "pass" } });
@@ -43,4 +43,23 @@ await test("GET - server.inject", async (t) => {
4343
t.assert.equal(failingRes.url, "/param/fail");
4444
t.assert.deepStrictEqual<SpecificBodyType<routeType, 400>>(failingRes.body, { success: false, message: "failed" });
4545
});
46+
47+
await t.test("POST /post", async (t: TestContext) => {
48+
type routeType = MatchingRoute<Routes, "POST", "/post">;
49+
50+
const passingRes = await client.post("/post", { payload: { flag: true } });
51+
t.assert.equal(passingRes.status, 200);
52+
t.assert.equal(passingRes.url, "/post");
53+
t.assert.deepStrictEqual<SpecificBodyType<routeType, 200>>(passingRes.body, { flag: true });
54+
55+
// @ts-expect-error Type 'undefined' is not assignable to type 'PostPostPayload'
56+
const failingRes = await client.post("/post", { payload: undefined });
57+
t.assert.equal(failingRes.status, 400);
58+
t.assert.equal(failingRes.url, "/post");
59+
t.assert.deepStrictEqual<SpecificBodyType<routeType, 400>>(failingRes.body, {
60+
error: "Bad Request",
61+
message: "Invalid request payload input",
62+
statusCode: 400,
63+
});
64+
});
4665
});

0 commit comments

Comments
 (0)