Skip to content

Commit 3cd0025

Browse files
authored
Improve API types (#398)
* Improve API types * Fix
1 parent d05fa5e commit 3cd0025

File tree

7 files changed

+208
-130
lines changed

7 files changed

+208
-130
lines changed

apps/api/modules/questions/questions.routes.ts

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,34 @@ import { TypeBoxTypeProvider } from "@fastify/type-provider-typebox";
22
import { FastifyInstance, FastifyPluginAsync } from "fastify";
33
import {
44
deleteQuestionByIdSchema,
5-
getQuestionByIdSchema,
6-
getQuestionsSchema,
7-
patchQuestionByIdSchema,
8-
postQuestionsSchema,
5+
generateGetQuestionsSchema,
6+
generatePatchQuestionByIdSchema,
7+
generatePostQuestionsSchema,
8+
generateGetQuestionByIdSchema,
99
} from "./questions.schemas.js";
10-
import { validateCategory, validateLevels, validateStatus } from "./questions.validators.js";
1110

1211
const questionsPlugin: FastifyPluginAsync = async (fastify) => {
1312
await fastify.register(import("./questions.utils.js"));
1413

14+
const [categories, levels, statuses] = await Promise.all([
15+
fastify.questionsGetCategories(),
16+
fastify.questionsGetLevels(),
17+
fastify.questionsGetStatuses(),
18+
]);
19+
const args = {
20+
categories,
21+
levels,
22+
statuses,
23+
};
24+
1525
fastify.withTypeProvider<TypeBoxTypeProvider>().route({
1626
url: "/questions",
1727
method: "GET",
18-
schema: getQuestionsSchema,
28+
schema: generateGetQuestionsSchema(args),
1929
async handler(request, reply) {
2030
const { category, level, status = "accepted", limit, offset, order, orderBy } = request.query;
2131
const levels = level?.split(",");
2232

23-
await Promise.all([
24-
validateCategory(fastify, category),
25-
validateLevels(fastify, levels),
26-
validateStatus(fastify, status),
27-
]);
28-
2933
const where = {
3034
...(category && { categoryId: category }),
3135
...(levels && { levelId: { in: levels } }),
@@ -98,12 +102,10 @@ const questionsPlugin: FastifyPluginAsync = async (fastify) => {
98102
fastify.withTypeProvider<TypeBoxTypeProvider>().route({
99103
url: "/questions",
100104
method: "POST",
101-
schema: postQuestionsSchema,
105+
schema: generatePostQuestionsSchema(args),
102106
async handler(request, reply) {
103107
const { question, level, category } = request.body;
104108

105-
await Promise.all([validateCategory(fastify, category), validateLevels(fastify, [level])]);
106-
107109
const newQuestion = await fastify.db.question.create({
108110
data: {
109111
question,
@@ -131,18 +133,12 @@ const questionsPlugin: FastifyPluginAsync = async (fastify) => {
131133
fastify.withTypeProvider<TypeBoxTypeProvider>().route({
132134
url: "/questions/:id",
133135
method: "PATCH",
134-
schema: patchQuestionByIdSchema,
136+
schema: generatePatchQuestionByIdSchema(args),
135137
async handler(request, reply) {
136138
const { id } = request.params;
137139

138140
const { question, level, category, status } = request.body;
139141

140-
await Promise.all([
141-
validateCategory(fastify, category),
142-
validateLevels(fastify, [level]),
143-
validateStatus(fastify, status),
144-
]);
145-
146142
const q = await fastify.db.question.update({
147143
where: { id },
148144
data: {
@@ -189,7 +185,7 @@ const questionsPlugin: FastifyPluginAsync = async (fastify) => {
189185
fastify.withTypeProvider<TypeBoxTypeProvider>().route({
190186
url: "/questions/:id",
191187
method: "GET",
192-
schema: getQuestionByIdSchema,
188+
schema: generateGetQuestionByIdSchema(args),
193189
async handler(request, reply) {
194190
const { id } = request.params;
195191

apps/api/modules/questions/questions.schemas.ts

Lines changed: 145 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,164 @@
11
import { Type, Static } from "@sinclair/typebox";
22

3-
const getQuestionsQuerySchema = Type.Object({
4-
category: Type.Optional(Type.String()),
5-
status: Type.Optional(Type.String()),
6-
level: Type.Optional(Type.String({ pattern: "^(\\w+,?)+$" })),
7-
limit: Type.Optional(Type.Integer()),
8-
offset: Type.Optional(Type.Integer()),
9-
orderBy: Type.Optional(
10-
Type.Union([Type.Literal("acceptedAt"), Type.Literal("level"), Type.Literal("votesCount")]),
11-
),
12-
order: Type.Optional(Type.Union([Type.Literal("asc"), Type.Literal("desc")])),
13-
});
14-
export type GetQuestionsQuery = Static<typeof getQuestionsQuerySchema>;
3+
const generateGetQuestionsQuerySchema = <
4+
Categories extends readonly string[],
5+
Levels extends readonly string[],
6+
Statuses extends readonly string[],
7+
>(args: {
8+
categories: Categories;
9+
levels: Levels;
10+
statuses: Statuses;
11+
}) =>
12+
Type.Object({
13+
category: Type.Optional(Type.Union(args.categories.map((val) => Type.Literal(val)))),
14+
status: Type.Optional(Type.Union(args.statuses.map((val) => Type.Literal(val)))),
15+
level: Type.Optional(Type.String({ pattern: `^([${args.levels.join("|")}],?)+$` })),
16+
limit: Type.Optional(Type.Integer()),
17+
offset: Type.Optional(Type.Integer()),
18+
orderBy: Type.Optional(
19+
Type.Union([Type.Literal("acceptedAt"), Type.Literal("level"), Type.Literal("votesCount")]),
20+
),
21+
order: Type.Optional(Type.Union([Type.Literal("asc"), Type.Literal("desc")])),
22+
});
23+
export type GetQuestionsQuery = Static<ReturnType<typeof generateGetQuestionsQuerySchema>>;
1524
export type GetQuestionsOrderBy = GetQuestionsQuery["orderBy"];
1625
export type GetQuestionsOrder = GetQuestionsQuery["order"];
1726

18-
const questionShape = {
19-
id: Type.Integer(),
20-
question: Type.String(),
21-
_categoryId: Type.String(),
22-
_levelId: Type.String(),
23-
_statusId: Type.String(),
24-
acceptedAt: Type.Optional(Type.String({ format: "date-time" })),
25-
} as const;
27+
const generateQuestionShape = <
28+
Categories extends readonly string[],
29+
Levels extends readonly string[],
30+
Statuses extends readonly string[],
31+
>(args: {
32+
categories: Categories;
33+
levels: Levels;
34+
statuses: Statuses;
35+
}) => {
36+
return {
37+
id: Type.Integer(),
38+
question: Type.String(),
39+
_categoryId: Type.Union(args.categories.map((val) => Type.Literal(val))),
40+
_levelId: Type.Union(args.levels.map((val) => Type.Literal(val))),
41+
_statusId: Type.Union(args.statuses.map((val) => Type.Literal(val))),
42+
acceptedAt: Type.Optional(Type.String({ format: "date-time" })),
43+
} as const;
44+
};
2645

27-
const createQuestionShape = {
28-
question: Type.String(),
29-
level: Type.String(),
30-
category: Type.String(),
46+
const generateCreateQuestionShape = <
47+
Categories extends readonly string[],
48+
Levels extends readonly string[],
49+
Statuses extends readonly string[],
50+
>(args: {
51+
categories: Categories;
52+
levels: Levels;
53+
statuses: Statuses;
54+
}) => {
55+
return {
56+
question: Type.String(),
57+
level: Type.Union(args.levels.map((val) => Type.Literal(val))),
58+
category: Type.Union(args.categories.map((val) => Type.Literal(val))),
59+
};
3160
};
3261

33-
const questionResponseSchema = Type.Object({
34-
...questionShape,
35-
votesCount: Type.Integer(),
36-
currentUserVotedOn: Type.Boolean(),
37-
});
62+
const generateQuestionResponseSchema = <
63+
Categories extends readonly string[],
64+
Levels extends readonly string[],
65+
Statuses extends readonly string[],
66+
>(args: {
67+
categories: Categories;
68+
levels: Levels;
69+
statuses: Statuses;
70+
}) =>
71+
Type.Object({
72+
...generateQuestionShape(args),
73+
votesCount: Type.Integer(),
74+
currentUserVotedOn: Type.Boolean(),
75+
});
3876

39-
export const getQuestionsSchema = {
40-
querystring: getQuestionsQuerySchema,
41-
response: {
42-
200: Type.Object({
43-
data: Type.Array(questionResponseSchema),
44-
meta: Type.Object({
45-
total: Type.Integer(),
77+
export const generateGetQuestionsSchema = <
78+
Categories extends readonly string[],
79+
Levels extends readonly string[],
80+
Statuses extends readonly string[],
81+
>(args: {
82+
categories: Categories;
83+
levels: Levels;
84+
statuses: Statuses;
85+
}) => {
86+
return {
87+
querystring: generateGetQuestionsQuerySchema(args),
88+
response: {
89+
200: Type.Object({
90+
data: Type.Array(generateQuestionResponseSchema(args)),
91+
meta: Type.Object({
92+
total: Type.Integer(),
93+
}),
4694
}),
47-
}),
48-
},
49-
} as const;
95+
},
96+
} as const;
97+
};
5098

51-
export const postQuestionsSchema = {
52-
body: Type.Object(createQuestionShape),
53-
response: {
54-
200: Type.Object({
55-
data: questionResponseSchema,
56-
}),
57-
},
58-
} as const;
99+
export const generatePostQuestionsSchema = <
100+
Categories extends readonly string[],
101+
Levels extends readonly string[],
102+
Statuses extends readonly string[],
103+
>(args: {
104+
categories: Categories;
105+
levels: Levels;
106+
statuses: Statuses;
107+
}) => {
108+
return {
109+
body: Type.Object(generateCreateQuestionShape(args)),
110+
response: {
111+
200: Type.Object({
112+
data: generateQuestionResponseSchema(args),
113+
}),
114+
},
115+
} as const;
116+
};
59117

60-
export const patchQuestionByIdSchema = {
61-
params: Type.Object({
62-
id: Type.Integer(),
63-
}),
64-
body: Type.Object({ ...createQuestionShape, status: Type.String() }),
65-
response: {
66-
200: Type.Object({
67-
data: questionResponseSchema,
118+
export const generatePatchQuestionByIdSchema = <
119+
Categories extends readonly string[],
120+
Levels extends readonly string[],
121+
Statuses extends readonly string[],
122+
>(args: {
123+
categories: Categories;
124+
levels: Levels;
125+
statuses: Statuses;
126+
}) => {
127+
return {
128+
params: Type.Object({
129+
id: Type.Integer(),
68130
}),
69-
},
131+
body: Type.Object({
132+
...generateCreateQuestionShape(args),
133+
status: Type.Union(args.statuses.map((val) => Type.Literal(val))),
134+
}),
135+
response: {
136+
200: Type.Object({
137+
data: generateQuestionResponseSchema(args),
138+
}),
139+
},
140+
};
70141
};
71142

72-
export const getQuestionByIdSchema = {
73-
params: Type.Object({
74-
id: Type.Integer(),
75-
}),
76-
response: {
77-
200: Type.Object({
78-
data: questionResponseSchema,
143+
export const generateGetQuestionByIdSchema = <
144+
Categories extends readonly string[],
145+
Levels extends readonly string[],
146+
Statuses extends readonly string[],
147+
>(args: {
148+
categories: Categories;
149+
levels: Levels;
150+
statuses: Statuses;
151+
}) => {
152+
return {
153+
params: Type.Object({
154+
id: Type.Integer(),
79155
}),
80-
},
156+
response: {
157+
200: Type.Object({
158+
data: generateQuestionResponseSchema(args),
159+
}),
160+
},
161+
};
81162
};
82163

83164
export const deleteQuestionByIdSchema = {

apps/api/modules/questions/questions.validators.ts

Lines changed: 0 additions & 22 deletions
This file was deleted.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
INSERT INTO "QuestionStatus" (id, "createdAt", "updatedAt")
2+
VALUES
3+
('pending', NOW(), NOW()),
4+
('accepted', NOW(), NOW())
5+
ON CONFLICT (id) DO NOTHING;
6+
7+
INSERT INTO "QuestionCategory" (id, "createdAt", "updatedAt")
8+
VALUES
9+
('html', NOW(), NOW()),
10+
('css', NOW(), NOW()),
11+
('js', NOW(), NOW()),
12+
('angular', NOW(), NOW()),
13+
('react', NOW(), NOW()),
14+
('git', NOW(), NOW()),
15+
('other', NOW(), NOW())
16+
ON CONFLICT (id) DO NOTHING;
17+
18+
INSERT INTO "QuestionLevel" (id, "createdAt", "updatedAt")
19+
VALUES
20+
('junior', NOW(), NOW()),
21+
('mid', NOW(), NOW()),
22+
('senior', NOW(), NOW())
23+
ON CONFLICT (id) DO NOTHING;

apps/app/src/app/foo/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export default async function FooPage() {
1010
<QuestionItem
1111
key={id}
1212
title={question}
13-
level={_levelId as "junior"}
13+
level={_levelId}
1414
creationDate={new Date(acceptedAt || "")}
1515
votes={votesCount}
1616
voted={id % 2 === 0}

packages/openapi-types/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"types": "index.ts",
66
"license": "MIT",
77
"scripts": {
8-
"generate": "openapi-typescript https://staging-api.devfaq.pl/documentation/json --output types.ts"
8+
"generate": "openapi-typescript http://api.devfaq.localhost:3002/documentation/json --output types.ts"
99
},
1010
"devDependencies": {
1111
"openapi-typescript": "6.1.0",

0 commit comments

Comments
 (0)