Skip to content

Commit 528fec6

Browse files
committed
Generate Zod types from the GraphQL schema TypeScript types!
I wrote the Zod types manually so far, but I'd like to get some code generation going to help. We can't remove the manually written Zod types just yet, since they're more specific than the generated ones, but hopefully soon!
1 parent 4922dca commit 528fec6

20 files changed

+655
-189
lines changed

workspaces/leetcode-api/graphql-codegen.config.ts

Lines changed: 68 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type { CodegenConfig } from "@graphql-codegen/cli";
2+
import type { Types as GraphQLCodegen } from "@graphql-codegen/plugin-helpers";
3+
import immutableUpdate from "immutability-helper";
24

3-
const commonTypeScriptPluginConfig = {
5+
const commonTypeScriptPluginConfig: GraphQLCodegen.PluginConfig = {
46
arrayInputCoercion: false,
57
enumsAsTypes: true,
68
defaultScalarType: "unknown",
@@ -10,50 +12,91 @@ const commonTypeScriptPluginConfig = {
1012
// TODO: add strictScalars: true
1113
};
1214

13-
const headerPlugin = {
15+
const headerPlugin: GraphQLCodegen.OutputConfig = {
1416
add: {
1517
content: `
1618
// THIS FILE IS GENERATED! DO NOT MODIFY IT MANUALLY!!
1719
// Instead, update the generation process or inputs and run \`yarn codegen\`.
1820
`,
21+
placement: "prepend",
22+
},
23+
};
24+
25+
const nearOperationFilePreset: GraphQLCodegen.ConfiguredOutput = {
26+
preset: "near-operation-file",
27+
presetConfig: {
28+
// We enable `globalNamespace` below so that we don't import the base
29+
// types through the preset, but let the specified plugins add the imports
30+
// if necessary. This allows us to make sure the base types import is
31+
// below the header, and it uses the ".ts" extension.
32+
baseTypesPath: "<not-used-but-cannot-be-empty>",
33+
34+
extension: ".generated.ts",
35+
fileName: null as string | null,
36+
},
37+
plugins: [headerPlugin],
38+
config: {
39+
...commonTypeScriptPluginConfig,
40+
41+
// Specified plugins will handle the imports and exports!
42+
globalNamespace: true,
43+
noExport: true,
1944
},
2045
};
2146

2247
const config: CodegenConfig = {
2348
schema: "schema.graphql",
24-
documents: ["src/**/*.graphql"],
49+
documents: ["src/api/**/query.graphql"],
2550
overwrite: true,
2651
emitLegacyCommonJSImports: false,
2752
generates: {
53+
// Generate the base types file!
2854
"src/graphqlTypes.generated.ts": {
2955
plugins: [headerPlugin, "typescript"],
3056
config: commonTypeScriptPluginConfig,
3157
},
32-
"src/": {
33-
preset: "near-operation-file",
34-
presetConfig: {
35-
// The base types are not imported because of the use of
36-
// `globalNamespace` below, instead our custom plugin will add the
37-
// import, so that it can end up _below_ the header.
38-
baseTypesPath: "<not-used-but-cannot-be-empty>",
39-
40-
extension: ".generated.ts",
41-
fileName: "fetchGraphQL",
58+
59+
// Generate a file for the query variables and result types, near each
60+
// operation.
61+
"src/api/**/queryTypes.generated.ts": immutableUpdate(
62+
nearOperationFilePreset,
63+
{
64+
presetConfig: { fileName: { $set: "queryTypes" } },
65+
plugins: {
66+
$push: [
67+
// Explicitly add the base types import, since the preset was
68+
// tricked into not adding it.
69+
{
70+
add: {
71+
content:
72+
'\n\nimport type * as Types from "../../graphqlTypes.generated.ts";\n\n',
73+
placement: "content",
74+
},
75+
},
76+
77+
// Generate TypeScript operations types, overriding the
78+
// `globalNamespace` which was set in the preset.
79+
{ "typescript-operations": { globalNamespace: false } },
80+
],
81+
},
82+
83+
// Do export the types.
84+
config: { noExport: { $set: false } },
4285
},
43-
plugins: [
44-
headerPlugin,
45-
{ "typescript-operations": { globalNamespace: false } },
46-
"./src/scripts/codegen/graphqlCodegenPlugin.ts",
47-
],
48-
config: {
49-
...commonTypeScriptPluginConfig,
50-
51-
// Our custom plugin will handle the imports and exports!
52-
globalNamespace: true,
53-
noExport: true,
86+
),
87+
88+
// Generate a small SDK for each operation using our custom plugin.
89+
"src/api/**/fetchGraphQL.generated.ts": immutableUpdate(
90+
nearOperationFilePreset,
91+
{
92+
presetConfig: { fileName: { $set: "fetchGraphQL" } },
93+
plugins: {
94+
$push: ["./src/scripts/codegen/graphqlCodegenPlugin.ts"],
95+
},
5496
},
55-
},
97+
),
5698
},
99+
57100
hooks: {
58101
afterAllFileWrite: ["prettier --write"],
59102
},

workspaces/leetcode-api/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,12 @@
4242
"cross-env": "7.0.3",
4343
"eslint": "9.12.0",
4444
"graphql-query-compress": "1.2.4",
45+
"immutability-helper": "patch:immutability-helper@npm%3A3.1.1#~/.yarn/patches/immutability-helper-npm-3.1.1-482f1f8f58.patch",
4546
"jest": "29.7.0",
4647
"prettier": "3.3.3",
4748
"ts-jest": "29.2.5",
49+
"ts-to-zod": "3.13.0",
4850
"tsx": "4.19.1",
49-
"type-fest": "4.26.1",
5051
"typescript": "5.6.2"
5152
}
5253
}

workspaces/leetcode-api/src/api/active-daily-coding-challenge-question/fetchGraphQL.generated.ts

Lines changed: 34 additions & 24 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

workspaces/leetcode-api/src/api/active-daily-coding-challenge-question/main.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,25 @@ const questionZodType = z.object({
1616
titleSlug: questionTitleSlugZodType,
1717
});
1818

19-
const activeDailyCodingChallengeQuestionZodType = z
20-
.object({
21-
activeDailyCodingChallengeQuestion: z.object({
22-
date: z
23-
.string()
24-
.trim()
25-
.regex(/^\d{4}-\d{2}-\d{2}$/),
26-
question: questionZodType,
27-
}),
28-
})
29-
.transform((data) => data.activeDailyCodingChallengeQuestion);
19+
const activeDailyCodingChallengeQuestionZodType = z.object({
20+
date: z
21+
.string()
22+
.trim()
23+
.regex(/^\d{4}-\d{2}-\d{2}$/),
24+
question: questionZodType,
25+
});
3026

3127
export type ActiveDailyCodingChallengeQuestion = z.infer<
3228
typeof activeDailyCodingChallengeQuestionZodType
3329
>;
3430

3531
export async function fetchActiveDailyCodingChallengeQuestionWithoutDateValidation(): Promise<ActiveDailyCodingChallengeQuestion> {
3632
// TODO: have a way to omit variables when there aren't any
37-
const data = await fetchGraphQL({});
33+
const { activeDailyCodingChallengeQuestion } = await fetchGraphQL({});
3834

39-
return activeDailyCodingChallengeQuestionZodType.parse(data);
35+
return activeDailyCodingChallengeQuestionZodType.parse(
36+
activeDailyCodingChallengeQuestion,
37+
);
4038
}
4139

4240
export async function fetchActiveDailyCodingChallengeQuestionWithDateValidation({

workspaces/leetcode-api/src/api/active-daily-coding-challenge-question/queryTypes.generated.ts

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

workspaces/leetcode-api/src/api/question-list/fetchGraphQL.generated.ts

Lines changed: 37 additions & 27 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

workspaces/leetcode-api/src/api/question-list/main.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,10 @@ export type QuestionListQuestion = z.infer<typeof questionZodType>;
1818

1919
const questionListZodType = z
2020
.object({
21-
questionList: z.object({
22-
data: z.array(questionZodType),
23-
totalNum: z.number().int().nonnegative(),
24-
}),
21+
data: z.array(questionZodType),
22+
totalNum: z.number().int().nonnegative(),
2523
})
26-
.transform(({ questionList: { data, totalNum } }) => ({
24+
.transform(({ data, totalNum }) => ({
2725
questions: data,
2826
totalNum,
2927
}));
@@ -52,12 +50,12 @@ export async function fetchQuestionList({
5250
limit: number;
5351
skip: number;
5452
}): Promise<QuestionList> {
55-
const data = await fetchGraphQL({
53+
const { questionList } = await fetchGraphQL({
5654
categorySlug,
5755
filters,
5856
limit,
5957
skip,
6058
});
6159

62-
return questionListZodType.parse(data);
60+
return questionListZodType.parse(questionList);
6361
}

workspaces/leetcode-api/src/api/question-list/queryTypes.generated.ts

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)