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+ }
0 commit comments