Skip to content

Commit 8e2b949

Browse files
committed
initial commit
0 parents  commit 8e2b949

File tree

10 files changed

+426
-0
lines changed

10 files changed

+426
-0
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules
2+
dist
3+
.npmignore
4+
gen/schemas.ts
5+
package-lock.json

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2022 codehex
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

babel.config.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module.exports = {
2+
presets: [
3+
[
4+
"@babel/preset-env",
5+
{ targets: { node: process.versions.node.split(".")[0] } },
6+
],
7+
"@babel/preset-typescript",
8+
],
9+
};

codegen.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
overwrite: true
2+
schema: "./test.graphql"
3+
generates:
4+
gen/schemas.ts:
5+
plugins:
6+
- typescript
7+
- ./dist/index.js:
8+
schema: yup

gen/.gitkeep

Whitespace-only changes.

package.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"name": "graphql-codegen-validation-schema",
3+
"version": "1.0.0",
4+
"description": "A plugin for GraphQL codegen to generate form validation schema from your GraphQL schema",
5+
"main": "./dist/index.js",
6+
"scripts": {
7+
"test": "echo \"Error: no test specified\" && exit 1",
8+
"generate": "tsc && graphql-codegen"
9+
},
10+
"keywords": [
11+
"gql",
12+
"generator",
13+
"yup",
14+
"zod",
15+
"code",
16+
"types",
17+
"graphql",
18+
"codegen",
19+
"apollo",
20+
"node",
21+
"types",
22+
"typings"
23+
],
24+
"author": "codehex",
25+
"license": "MIT",
26+
"devDependencies": {
27+
"@graphql-codegen/cli": "^2.3.1",
28+
"@graphql-codegen/typescript": "^2.4.2",
29+
"@tsconfig/recommended": "^1.0.1",
30+
"typescript": "^4.5.4",
31+
"yup": "^0.32.11"
32+
},
33+
"dependencies": {
34+
"@graphql-codegen/plugin-helpers": "^2.3.2",
35+
"@graphql-codegen/schema-ast": "^2.4.1",
36+
"@graphql-codegen/visitor-plugin-common": "^2.5.2",
37+
"@graphql-tools/utils": "^8.6.1",
38+
"graphql": "^15.8.0"
39+
}
40+
}

src/index.ts

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import { ValidationSchema, ValidationSchemaPluginConfig } from "./types";
2+
import { PluginFunction, Types } from "@graphql-codegen/plugin-helpers";
3+
import { indent } from "@graphql-codegen/visitor-plugin-common";
4+
import {
5+
EnumTypeDefinitionNode,
6+
GraphQLSchema,
7+
InputObjectTypeDefinitionNode,
8+
InputValueDefinitionNode,
9+
ScalarTypeDefinitionNode,
10+
visit,
11+
NamedTypeNode,
12+
TypeNode,
13+
ListTypeNode,
14+
NonNullTypeNode,
15+
NameNode,
16+
} from "graphql";
17+
import { transformSchemaAST } from "@graphql-codegen/schema-ast";
18+
19+
export const plugin: PluginFunction<ValidationSchemaPluginConfig> = async (
20+
schema: GraphQLSchema,
21+
_documents: Types.DocumentFile[],
22+
config: ValidationSchemaPluginConfig
23+
): Promise<Types.PluginOutput> => {
24+
const { inputObjects, enums, scalars } = retrieveSchema(schema, config);
25+
26+
return {
27+
prepend: [importSchema(config.schema)],
28+
content: [
29+
generateYupSchema({ inputObjects, enums, scalars }),
30+
].join("\n"),
31+
};
32+
};
33+
34+
const importSchema = (schema?: ValidationSchema): string => {
35+
if (schema === "yup") {
36+
return `import * as yup from 'yup'`;
37+
}
38+
// TODO(codehex): support zod
39+
return `import * as yup from 'yup'`;
40+
};
41+
42+
interface Nodes {
43+
inputObjects: InputObjectTypeDefinitionNode[];
44+
enums: Record<string, EnumTypeDefinitionNode>;
45+
scalars: Record<string, ScalarTypeDefinitionNode>;
46+
}
47+
48+
const retrieveSchema = (
49+
schema: GraphQLSchema,
50+
config: ValidationSchemaPluginConfig
51+
): Nodes => {
52+
const { ast } = transformSchemaAST(schema, config);
53+
54+
const inputObjects: InputObjectTypeDefinitionNode[] = [];
55+
const enums: Record<string, EnumTypeDefinitionNode> = {};
56+
const scalars: Record<string, ScalarTypeDefinitionNode> = {};
57+
58+
visit(ast, {
59+
leave: {
60+
InputObjectTypeDefinition: (node) => {
61+
inputObjects.unshift(node);
62+
},
63+
EnumTypeDefinition: (node) => {
64+
if (node.values) {
65+
enums[node.name.value] = node;
66+
}
67+
},
68+
ScalarTypeDefinition: (node) => {
69+
scalars[node.name.value] = node;
70+
},
71+
},
72+
});
73+
74+
return { inputObjects, enums, scalars };
75+
};
76+
77+
const generateYupSchema = ({ inputObjects, enums, scalars }: Nodes): string => {
78+
return inputObjects
79+
.map((inputObject) =>
80+
generateInputObjectYupSchema({ inputObject, enums, scalars })
81+
)
82+
.join("\n\n");
83+
};
84+
85+
interface InputObjectGeneratorParams {
86+
inputObject: InputObjectTypeDefinitionNode;
87+
enums: Record<string, EnumTypeDefinitionNode>;
88+
scalars: Record<string, ScalarTypeDefinitionNode>;
89+
}
90+
91+
const generateInputObjectYupSchema = ({
92+
inputObject,
93+
enums,
94+
scalars,
95+
}: InputObjectGeneratorParams): string => {
96+
const name = inputObject.name.value;
97+
const { fields } = inputObject;
98+
if (!fields) return ``;
99+
100+
const shape = fields
101+
.map((field) =>
102+
generateInputObjectFieldYupSchema({
103+
field,
104+
enums,
105+
scalars,
106+
indentCount: 2,
107+
})
108+
)
109+
.join(",\n");
110+
return [
111+
`export function ${name}Schema(): yup.SchemaOf<${name}> {`,
112+
indent(`return yup.object({`),
113+
shape,
114+
indent("})"),
115+
`}`,
116+
].join("\n");
117+
};
118+
119+
interface InputObjectFieldGeneratorParams {
120+
field: InputValueDefinitionNode;
121+
enums: Record<string, EnumTypeDefinitionNode>;
122+
scalars: Record<string, ScalarTypeDefinitionNode>;
123+
indentCount?: number;
124+
}
125+
126+
const generateInputObjectFieldYupSchema = ({
127+
field,
128+
enums,
129+
scalars,
130+
indentCount,
131+
}: InputObjectFieldGeneratorParams): string => {
132+
// TOOD(codehex): handle directive
133+
let schema = generateInputObjectFieldTypeYupSchema({
134+
type: field.type,
135+
enums,
136+
scalars,
137+
})
138+
return indent(
139+
`${field.name.value}: ${maybeLazy(field.type, schema)}`,
140+
indentCount
141+
);
142+
};
143+
144+
interface InputObjectFieldTypeGeneratorParams {
145+
type: TypeNode;
146+
enums: Record<string, EnumTypeDefinitionNode>;
147+
scalars: Record<string, ScalarTypeDefinitionNode>;
148+
}
149+
150+
const generateInputObjectFieldTypeYupSchema = ({
151+
type,
152+
enums,
153+
scalars,
154+
}: InputObjectFieldTypeGeneratorParams): string => {
155+
if (isListType(type)) {
156+
return `yup.array().of(${generateInputObjectFieldTypeYupSchema({
157+
type: type.type,
158+
enums,
159+
scalars,
160+
})})`;
161+
}
162+
if (isNonNullType(type)) {
163+
const schema = generateInputObjectFieldTypeYupSchema({
164+
type: type.type,
165+
enums,
166+
scalars,
167+
})
168+
return maybeLazy(type.type, `${schema}.required()`);
169+
}
170+
if (isNamedType(type)) {
171+
return generateNameNodeYupSchema({
172+
node: type.name,
173+
enums,
174+
scalars,
175+
});
176+
}
177+
console.warn("unhandled type:", type);
178+
return "";
179+
};
180+
181+
interface NameNodeGeneratorParams {
182+
node: NameNode;
183+
enums: Record<string, EnumTypeDefinitionNode>;
184+
scalars: Record<string, ScalarTypeDefinitionNode>;
185+
}
186+
187+
const generateNameNodeYupSchema = ({
188+
node,
189+
enums,
190+
scalars,
191+
}: NameNodeGeneratorParams): string => {
192+
if (isRef(node.value)) {
193+
return `${node.value}Schema()`;
194+
}
195+
if (isString(node.value)) {
196+
return `yup.string()`;
197+
}
198+
if (isBoolean(node.value)) {
199+
return `yup.boolean()`;
200+
}
201+
if (isNumber(node.value)) {
202+
return `yup.number()`;
203+
}
204+
if (enums[node.value]) {
205+
const enumdef = enums[node.value];
206+
return `yup.mixed().oneOf(Object.values(${enumdef.name.value}))`;
207+
}
208+
if (scalars[node.value]) {
209+
console.warn("unhandled scalar:", scalars[node.value]);
210+
return "yup.mixed()";
211+
}
212+
console.warn("unhandled name:", node);
213+
return "yup.mixed()";
214+
};
215+
216+
const isListType = (typ: TypeNode): typ is ListTypeNode =>
217+
typ.kind === "ListType";
218+
const isNonNullType = (typ: TypeNode): typ is NonNullTypeNode =>
219+
typ.kind === "NonNullType";
220+
const isNamedType = (typ: TypeNode): typ is NamedTypeNode =>
221+
typ.kind === "NamedType";
222+
223+
const isRef = (kind: string) => kind.includes("Input");
224+
const isBoolean = (kind: string) => kind === "Boolean";
225+
const isString = (kind: string) => ["ID", "String"].includes(kind);
226+
const isNumber = (kind: string) => ["Int", "Float"].includes(kind);
227+
228+
const maybeLazy = (type: TypeNode, schema: string): string => {
229+
if (isNamedType(type) && isRef(type.name.value)) {
230+
// https://github.com/jquense/yup/issues/1283#issuecomment-786559444
231+
return `yup.lazy(() => ${schema}) as never`;
232+
}
233+
return schema
234+
}

src/types.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { TypeScriptPluginConfig } from '@graphql-codegen/typescript';
2+
3+
// THIS ARE TYPES IN THE CONTEXT OF THE FIELD
4+
export const TYPE_LIST = "ListType";
5+
export const TYPE_NONULL = "NonNullType";
6+
export const TYPE_NAMED = "NamedType";
7+
8+
// THIS ARE TYPES OF FIELD VALUE
9+
export const TYPE_INPUT = "Input";
10+
export const TYPE_BOOLEAN = "Boolean";
11+
export const TYPE_STRINGS = ["ID", "String"];
12+
export const TYPE_NUMBERS = ["Int", "Float"];
13+
14+
export type ValidationSchema = "yup";
15+
16+
export interface ValidationSchemaPluginConfig extends TypeScriptPluginConfig {
17+
/**
18+
* @description specify generate schema.
19+
* @default yup
20+
*
21+
* @exampleMarkdown
22+
* ```yml
23+
* generates:
24+
* path/to/file.ts:
25+
* plugins:
26+
* - typescript
27+
* - graphql-codegen-validation-schema
28+
* config:
29+
* schema: yup
30+
* ```
31+
*/
32+
schema?: ValidationSchema;
33+
}
34+

0 commit comments

Comments
 (0)