├── .github ├── FUNDING.yml ├── renovate.json └── workflows │ ├── ci.yml │ └── issues.yml ├── .gitignore ├── .prettierignore ├── LICENSE ├── README.md ├── codegen.yml ├── eslint.config.mjs ├── example ├── myzod │ ├── README.md │ └── schemas.ts ├── test.graphql ├── types.ts ├── valibot │ └── schemas.ts ├── yup │ ├── README.md │ └── schemas.ts └── zod │ ├── README.md │ └── schemas.ts ├── package.json ├── pnpm-lock.yaml ├── src ├── config.ts ├── directive.ts ├── graphql.ts ├── index.ts ├── myzod │ └── index.ts ├── regexp.ts ├── schema_visitor.ts ├── types.ts ├── valibot │ └── index.ts ├── visitor.ts ├── yup │ └── index.ts └── zod │ └── index.ts ├── tests ├── directive.spec.ts ├── graphql.spec.ts ├── myzod.spec.ts ├── regexp.spec.ts ├── valibot.spec.ts ├── yup.spec.ts └── zod.spec.ts ├── tsconfig.cjs.json ├── tsconfig.esm.json ├── tsconfig.json ├── tsconfig.types.json └── vitest.config.ts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: Code-Hex 2 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "enabledManagers": ["npm"], 4 | "ignorePresets": [":prHourlyLimit2"], 5 | "timezone": "Asia/Tokyo", 6 | "automerge": true, 7 | "automergeType": "pr", 8 | "platformAutomerge": true, 9 | "dependencyDashboard": false, 10 | "onboarding": false, 11 | "prConcurrentLimit": 5 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: pnpm/action-setup@v3 13 | with: 14 | package_json_file: ./package.json 15 | - name: Setup Node 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: current 19 | cache: pnpm 20 | - run: pnpm install --frozen-lockfile 21 | - run: pnpm test 22 | env: 23 | CI: true 24 | eslint: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v3 28 | - uses: pnpm/action-setup@v3 29 | with: 30 | package_json_file: ./package.json 31 | - name: Setup Node 32 | uses: actions/setup-node@v3 33 | with: 34 | node-version: current 35 | cache: pnpm 36 | - run: pnpm install --frozen-lockfile 37 | - run: pnpm lint-fix 38 | - name: Auto commit fixed code 39 | id: auto-commit-action 40 | uses: stefanzweifel/git-auto-commit-action@v4 41 | with: 42 | commit_message: Apply auto lint-fix changes 43 | branch: ${{ github.head_ref }} 44 | # See: https://github.com/reviewdog/action-eslint/issues/152 45 | # - name: eslint 46 | # if: steps.auto-commit-action.outputs.changes_detected == 'false' 47 | # uses: reviewdog/action-eslint@v1 48 | # with: 49 | # github_token: ${{ secrets.GITHUB_TOKEN }} 50 | # level: warning 51 | # reporter: github-pr-review 52 | -------------------------------------------------------------------------------- /.github/workflows/issues.yml: -------------------------------------------------------------------------------- 1 | name: good first issue 2 | on: [issues] 3 | 4 | jobs: 5 | labels: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: Code-Hex/first-label-interaction@v1.0.3 9 | with: 10 | github-token: ${{ secrets.GITHUB_TOKEN }} 11 | issue-labels: '["good first issue"]' 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .npmignore 4 | package-lock.json 5 | .DS_Store 6 | tsconfig.tsbuildinfo 7 | yarn-error.log -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | ** -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-codegen-typescript-validation-schema 2 | 3 | [![Test](https://github.com/Code-Hex/graphql-codegen-typescript-validation-schema/actions/workflows/ci.yml/badge.svg)](https://github.com/Code-Hex/graphql-codegen-typescript-validation-schema/actions/workflows/ci.yml) [![npm version](https://badge.fury.io/js/graphql-codegen-typescript-validation-schema.svg)](https://badge.fury.io/js/graphql-codegen-typescript-validation-schema) 4 | 5 | [GraphQL code generator](https://github.com/dotansimha/graphql-code-generator) plugin to generate form validation schema from your GraphQL schema. 6 | 7 | - [x] support [yup](https://github.com/jquense/yup) 8 | - [x] support [zod](https://github.com/colinhacks/zod) 9 | - [x] support [myzod](https://github.com/davidmdm/myzod) 10 | - [x] support [valibot](https://valibot.dev/) 11 | 12 | ## Quick Start 13 | 14 | Start by installing this plugin and write simple plugin config; 15 | 16 | ```sh 17 | $ npm i graphql-codegen-typescript-validation-schema 18 | ``` 19 | 20 | ```yml 21 | generates: 22 | path/to/graphql.ts: 23 | plugins: 24 | - typescript 25 | - typescript-validation-schema # specify to use this plugin 26 | config: 27 | # You can put the config for typescript plugin here 28 | # see: https://www.graphql-code-generator.com/plugins/typescript 29 | strictScalars: true 30 | # Overrides built-in ID scalar to both input and output types as string. 31 | # see: https://the-guild.dev/graphql/codegen/plugins/typescript/typescript#scalars 32 | scalars: 33 | ID: string 34 | # You can also write the config for this plugin together 35 | schema: yup # or zod 36 | ``` 37 | 38 | It is recommended to write `scalars` config for built-in type `ID`, as in the yaml example shown above. For more information: [#375](https://github.com/Code-Hex/graphql-codegen-typescript-validation-schema/pull/375) 39 | 40 | You can check [example](https://github.com/Code-Hex/graphql-codegen-typescript-validation-schema/tree/main/example) directory if you want to see more complex config example or how is generated some files. 41 | 42 | The Q&A for each schema is written in the README in the respective example directory. 43 | 44 | ## Config API Reference 45 | 46 | ### `schema` 47 | 48 | type: `ValidationSchema` default: `'yup'` 49 | 50 | Specify generete validation schema you want. 51 | 52 | You can specify `yup` or `zod` or `myzod`. 53 | 54 | ```yml 55 | generates: 56 | path/to/graphql.ts: 57 | plugins: 58 | - typescript 59 | - typescript-validation-schema 60 | config: 61 | schema: yup 62 | ``` 63 | 64 | ### `importFrom` 65 | 66 | type: `string` 67 | 68 | When provided, import types from the generated typescript types file path. if not given, omit import statement. 69 | 70 | ```yml 71 | generates: 72 | path/to/graphql.ts: 73 | plugins: 74 | - typescript 75 | path/to/validation.ts: 76 | plugins: 77 | - typescript-validation-schema 78 | config: 79 | importFrom: ./graphql # path for generated ts code 80 | ``` 81 | 82 | Then the generator generates code with import statement like below. 83 | 84 | ```ts 85 | import { GeneratedInput } from './graphql' 86 | 87 | /* generates validation schema here */ 88 | ``` 89 | 90 | ### `schemaNamespacedImportName` 91 | 92 | type: `string` 93 | 94 | If defined, will use named imports from the specified module (defined in `importFrom`) rather than individual imports for each type. 95 | 96 | ```yml 97 | generates: 98 | path/to/types.ts: 99 | plugins: 100 | - typescript 101 | path/to/schemas.ts: 102 | plugins: 103 | - graphql-codegen-validation-schema 104 | config: 105 | schema: yup 106 | importFrom: ./path/to/types 107 | schemaNamespacedImportName: types 108 | ``` 109 | 110 | Then the generator generates code with import statement like below. 111 | 112 | ```ts 113 | import * as types from './graphql' 114 | 115 | /* generates validation schema here */ 116 | ``` 117 | 118 | ### `useTypeImports` 119 | 120 | type: `boolean` default: `false` 121 | 122 | Will use `import type {}` rather than `import {}` when importing generated TypeScript types. 123 | This gives compatibility with TypeScript's "importsNotUsedAsValues": "error" option. 124 | Should used in conjunction with `importFrom` option. 125 | 126 | ### `typesPrefix` 127 | 128 | type: `string` default: (empty) 129 | 130 | Prefixes all import types from generated typescript type. 131 | 132 | ```yml 133 | generates: 134 | path/to/graphql.ts: 135 | plugins: 136 | - typescript 137 | path/to/validation.ts: 138 | plugins: 139 | - typescript-validation-schema 140 | config: 141 | typesPrefix: I 142 | importFrom: ./graphql # path for generated ts code 143 | ``` 144 | 145 | Then the generator generates code with import statement like below. 146 | 147 | ```ts 148 | import { IGeneratedInput } from './graphql' 149 | 150 | /* generates validation schema here */ 151 | ``` 152 | 153 | ### `typesSuffix` 154 | 155 | type: `string` default: (empty) 156 | 157 | Suffixes all import types from generated typescript type. 158 | 159 | ```yml 160 | generates: 161 | path/to/graphql.ts: 162 | plugins: 163 | - typescript 164 | path/to/validation.ts: 165 | plugins: 166 | - typescript-validation-schema 167 | config: 168 | typesSuffix: I 169 | importFrom: ./graphql # path for generated ts code 170 | ``` 171 | 172 | Then the generator generates code with import statement like below. 173 | 174 | ```ts 175 | import { GeneratedInputI } from './graphql' 176 | 177 | /* generates validation schema here */ 178 | ``` 179 | 180 | ### `enumsAsTypes` 181 | 182 | type: `boolean` default: `false` 183 | 184 | Generates enum as TypeScript `type` instead of `enum`. 185 | 186 | ### `notAllowEmptyString` 187 | 188 | type: `boolean` default: `false` 189 | 190 | Generates validation string schema as do not allow empty characters by default. 191 | 192 | ### `scalarSchemas` 193 | 194 | type: `ScalarSchemas` 195 | 196 | Extends or overrides validation schema for the built-in scalars and custom GraphQL scalars. 197 | 198 | #### yup schema 199 | 200 | ```yml 201 | config: 202 | schema: yup 203 | scalarSchemas: 204 | Date: yup.date() 205 | Email: yup.string().email() 206 | ``` 207 | 208 | #### zod schema 209 | 210 | ```yml 211 | config: 212 | schema: zod 213 | scalarSchemas: 214 | Date: z.date() 215 | Email: z.string().email() 216 | ``` 217 | 218 | ### `defaultScalarTypeSchema` 219 | 220 | type: `string` 221 | 222 | Fallback scalar type for undefined scalar types in the schema not found in `scalarSchemas`. 223 | 224 | #### yup schema 225 | ```yml 226 | config: 227 | schema: yup 228 | defaultScalarSchema: yup.unknown() 229 | ``` 230 | 231 | #### zod schema 232 | ```yml 233 | config: 234 | schema: zod 235 | defaultScalarSchema: z.unknown() 236 | ``` 237 | 238 | ### `withObjectType` 239 | 240 | type: `boolean` default: `false` 241 | 242 | Generates validation schema with GraphQL type objects. But excludes `Query`, `Mutation`, `Subscription` objects. 243 | 244 | It is currently added for the purpose of using simple objects. See also [#20](https://github.com/Code-Hex/graphql-codegen-typescript-validation-schema/issues/20#issuecomment-1058969191), [#107](https://github.com/Code-Hex/graphql-codegen-typescript-validation-schema/issues/107). 245 | 246 | This option currently **does not support fragment** generation. If you are interested, send me PR would be greatly appreciated! 247 | 248 | ### `validationSchemaExportType` 249 | 250 | type: `ValidationSchemaExportType` default: `'function'` 251 | 252 | Specify validation schema export type. 253 | 254 | ### `useEnumTypeAsDefaultValue` 255 | 256 | type: `boolean` default: `false` 257 | 258 | Uses the full path of the enum type as the default value instead of the stringified value. 259 | 260 | ### `namingConvention` 261 | 262 | type: `NamingConventionMap` default: `{ enumValues: "change-case-all#pascalCase" }` 263 | 264 | Uses the full path of the enum type as the default value instead of the stringified value. 265 | 266 | Related: https://the-guild.dev/graphql/codegen/docs/config-reference/naming-convention#namingconvention 267 | 268 | ### `directives` 269 | 270 | type: `DirectiveConfig` 271 | 272 | Generates validation schema with more API based on directive schema. For example, yaml config and GraphQL schema is here. 273 | 274 | ```graphql 275 | input ExampleInput { 276 | email: String! @required(msg: "Hello, World!") @constraint(minLength: 50, format: "email") 277 | message: String! @constraint(startsWith: "Hello") 278 | } 279 | ``` 280 | 281 | #### yup schema 282 | 283 | ```yml 284 | generates: 285 | path/to/graphql.ts: 286 | plugins: 287 | - typescript 288 | - typescript-validation-schema 289 | config: 290 | schema: yup 291 | directives: 292 | # Write directives like 293 | # 294 | # directive: 295 | # arg1: schemaApi 296 | # arg2: ["schemaApi2", "Hello $1"] 297 | # 298 | # See more examples in `./tests/directive.spec.ts` 299 | # https://github.com/Code-Hex/graphql-codegen-typescript-validation-schema/blob/main/tests/directive.spec.ts 300 | required: 301 | msg: required 302 | constraint: 303 | minLength: min 304 | # Replace $1 with specified `startsWith` argument value of the constraint directive 305 | startsWith: [matches, /^$1/] 306 | format: 307 | # This example means `validation-schema: directive-arg` 308 | # directive-arg is supported String and Enum. 309 | email: email 310 | ``` 311 | 312 | Then generates yup validation schema like below. 313 | 314 | ```ts 315 | export function ExampleInputSchema(): yup.SchemaOf { 316 | return yup.object({ 317 | email: yup.string().defined().required('Hello, World!').min(50).email(), 318 | message: yup.string().defined().matches(/^Hello/) 319 | }) 320 | } 321 | ``` 322 | 323 | #### zod schema 324 | 325 | ```yml 326 | generates: 327 | path/to/graphql.ts: 328 | plugins: 329 | - typescript 330 | - typescript-validation-schema 331 | config: 332 | schema: zod 333 | directives: 334 | # Write directives like 335 | # 336 | # directive: 337 | # arg1: schemaApi 338 | # arg2: ["schemaApi2", "Hello $1"] 339 | # 340 | # See more examples in `./tests/directive.spec.ts` 341 | # https://github.com/Code-Hex/graphql-codegen-typescript-validation-schema/blob/main/tests/directive.spec.ts 342 | constraint: 343 | minLength: min 344 | # Replace $1 with specified `startsWith` argument value of the constraint directive 345 | startsWith: [regex, /^$1/, message] 346 | format: 347 | # This example means `validation-schema: directive-arg` 348 | # directive-arg is supported String and Enum. 349 | email: email 350 | ``` 351 | 352 | Then generates zod validation schema like below. 353 | 354 | ```ts 355 | export function ExampleInputSchema(): z.ZodSchema { 356 | return z.object({ 357 | email: z.string().min(50).email(), 358 | message: z.string().regex(/^Hello/, 'message') 359 | }) 360 | } 361 | ``` 362 | 363 | #### other schema 364 | 365 | Please see [example](https://github.com/Code-Hex/graphql-codegen-typescript-validation-schema/tree/main/example) directory. 366 | 367 | ## Notes 368 | 369 | Their is currently a compatibility issue with the client-preset. A workaround for this is to split the generation into two (one for client-preset and one for typescript-validation-schema). 370 | 371 | ```yml 372 | generates: 373 | path/to/graphql.ts: 374 | plugins: 375 | - typescript-validation-schema 376 | /path/to/graphql/: 377 | preset: 'client', 378 | plugins: 379 | ... 380 | ``` 381 | -------------------------------------------------------------------------------- /codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: ./example/test.graphql 3 | generates: 4 | example/types.ts: 5 | plugins: 6 | - typescript 7 | config: 8 | scalars: 9 | ID: string 10 | example/yup/schemas.ts: 11 | plugins: 12 | - ./dist/cjs/index.js: 13 | schema: yup 14 | importFrom: ../types 15 | withObjectType: true 16 | directives: 17 | required: 18 | msg: required 19 | # This is example using constraint directive. 20 | # see: https://github.com/confuser/graphql-constraint-directive 21 | constraint: 22 | minLength: min # same as ['min', '$1'] 23 | maxLength: max 24 | startsWith: [matches, /^$1/] 25 | endsWith: [matches, /$1$/] 26 | contains: [matches, /$1/] 27 | notContains: [matches, '/^((?!$1).)*$/'] 28 | pattern: [matches, /$1/] 29 | format: 30 | # For example, `@constraint(format: "uri")`. this case $1 will be "uri". 31 | # Therefore the generator generates yup schema `.url()` followed by `uri: 'url'` 32 | # If $1 does not match anywhere, the generator will ignore. 33 | uri: url 34 | email: email 35 | uuid: uuid 36 | # yup does not have `ipv4` API. If you want to add this, 37 | # you need to add the logic using `yup.addMethod`. 38 | # see: https://github.com/jquense/yup#addmethodschematype-schema-name-string-method--schema-void 39 | ipv4: ipv4 40 | min: [min, $1 - 1] 41 | max: [max, '$1 + 1'] 42 | exclusiveMin: min 43 | exclusiveMax: max 44 | scalars: 45 | ID: string 46 | example/zod/schemas.ts: 47 | plugins: 48 | - ./dist/cjs/index.js: 49 | schema: zod 50 | importFrom: ../types 51 | withObjectType: true 52 | directives: 53 | # Write directives like 54 | # 55 | # directive: 56 | # arg1: schemaApi 57 | # arg2: ["schemaApi2", "Hello $1"] 58 | # 59 | # See more examples in `./tests/directive.spec.ts` 60 | # https://github.com/Code-Hex/graphql-codegen-typescript-validation-schema/blob/main/tests/directive.spec.ts 61 | constraint: 62 | minLength: min 63 | # Replace $1 with specified `startsWith` argument value of the constraint directive 64 | startsWith: [regex, /^$1/, message] 65 | format: 66 | email: email 67 | scalars: 68 | ID: string 69 | example/myzod/schemas.ts: 70 | plugins: 71 | - ./dist/cjs/index.js: 72 | schema: myzod 73 | importFrom: ../types 74 | withObjectType: true 75 | directives: 76 | constraint: 77 | minLength: min 78 | # Replace $1 with specified `startsWith` argument value of the constraint directive 79 | startsWith: [pattern, /^$1/] 80 | format: 81 | email: email 82 | scalars: 83 | ID: string 84 | example/valibot/schemas.ts: 85 | plugins: 86 | - ./dist/cjs/index.js: 87 | schema: valibot 88 | importFrom: ../types 89 | withObjectType: true 90 | directives: 91 | # Write directives like 92 | # 93 | # directive: 94 | # arg1: schemaApi 95 | # arg2: ["schemaApi2", "Hello $1"] 96 | # 97 | # See more examples in `./tests/directive.spec.ts` 98 | # https://github.com/Code-Hex/graphql-codegen-typescript-validation-schema/blob/main/tests/directive.spec.ts 99 | constraint: 100 | minLength: minLength 101 | # Replace $1 with specified `startsWith` argument value of the constraint directive 102 | startsWith: [regex, /^$1/, message] 103 | format: 104 | email: email 105 | scalars: 106 | ID: string 107 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | 3 | export default antfu({ 4 | typescript: true, 5 | // `.eslintignore` is no longer supported in Flat config, use `ignores` instead 6 | ignores: [ 7 | 'dist/**', 8 | 'node_modules/**', 9 | 'example/**', 10 | 'vitest.config.ts', 11 | 'tsconfig.json', 12 | 'README.md', 13 | ], 14 | }, { 15 | rules: { 16 | 'style/semi': 'off', 17 | 'regexp/no-unused-capturing-group': 'off', 18 | }, 19 | }) 20 | -------------------------------------------------------------------------------- /example/myzod/README.md: -------------------------------------------------------------------------------- 1 | # Tips for myzod schema 2 | 3 | ## How to overwrite generated schema? 4 | 5 | Basically, I think [it does not support overwrite schema](https://github.com/davidmdm/myzod/issues/51) in myzod. However, [`and`](https://github.com/davidmdm/myzod#typeand) and [`or`](https://github.com/davidmdm/myzod#typeor) may helps you. 6 | 7 | See also: https://github.com/Code-Hex/graphql-codegen-typescript-validation-schema/issues/25#issuecomment-1086532098 -------------------------------------------------------------------------------- /example/myzod/schemas.ts: -------------------------------------------------------------------------------- 1 | import * as myzod from 'myzod' 2 | import { Admin, AttributeInput, ButtonComponentType, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, Guest, HttpInput, HttpMethod, LayoutInput, MyType, MyTypeFooArgs, Namer, PageInput, PageType, User } from '../types' 3 | 4 | export const definedNonNullAnySchema = myzod.object({}); 5 | 6 | export const ButtonComponentTypeSchema = myzod.enum(ButtonComponentType); 7 | 8 | export const EventOptionTypeSchema = myzod.enum(EventOptionType); 9 | 10 | export const HttpMethodSchema = myzod.enum(HttpMethod); 11 | 12 | export const PageTypeSchema = myzod.enum(PageType); 13 | 14 | export function AdminSchema(): myzod.Type { 15 | return myzod.object({ 16 | __typename: myzod.literal('Admin').optional(), 17 | lastModifiedAt: definedNonNullAnySchema.optional().nullable() 18 | }) 19 | } 20 | 21 | export function AttributeInputSchema(): myzod.Type { 22 | return myzod.object({ 23 | key: myzod.string().optional().nullable(), 24 | val: myzod.string().optional().nullable() 25 | }) 26 | } 27 | 28 | export function ComponentInputSchema(): myzod.Type { 29 | return myzod.object({ 30 | child: myzod.lazy(() => ComponentInputSchema().optional().nullable()), 31 | childrens: myzod.array(myzod.lazy(() => ComponentInputSchema().nullable())).optional().nullable(), 32 | event: myzod.lazy(() => EventInputSchema().optional().nullable()), 33 | name: myzod.string(), 34 | type: ButtonComponentTypeSchema 35 | }) 36 | } 37 | 38 | export function DropDownComponentInputSchema(): myzod.Type { 39 | return myzod.object({ 40 | dropdownComponent: myzod.lazy(() => ComponentInputSchema().optional().nullable()), 41 | getEvent: myzod.lazy(() => EventInputSchema()) 42 | }) 43 | } 44 | 45 | export function EventArgumentInputSchema(): myzod.Type { 46 | return myzod.object({ 47 | name: myzod.string().min(5), 48 | value: myzod.string().pattern(/^foo/) 49 | }) 50 | } 51 | 52 | export function EventInputSchema(): myzod.Type { 53 | return myzod.object({ 54 | arguments: myzod.array(myzod.lazy(() => EventArgumentInputSchema())), 55 | options: myzod.array(EventOptionTypeSchema).optional().nullable() 56 | }) 57 | } 58 | 59 | export function GuestSchema(): myzod.Type { 60 | return myzod.object({ 61 | __typename: myzod.literal('Guest').optional(), 62 | lastLoggedIn: definedNonNullAnySchema.optional().nullable() 63 | }) 64 | } 65 | 66 | export function HttpInputSchema(): myzod.Type { 67 | return myzod.object({ 68 | method: HttpMethodSchema.optional().nullable(), 69 | url: definedNonNullAnySchema 70 | }) 71 | } 72 | 73 | export function LayoutInputSchema(): myzod.Type { 74 | return myzod.object({ 75 | dropdown: myzod.lazy(() => DropDownComponentInputSchema().optional().nullable()) 76 | }) 77 | } 78 | 79 | export function MyTypeSchema(): myzod.Type { 80 | return myzod.object({ 81 | __typename: myzod.literal('MyType').optional(), 82 | foo: myzod.string().optional().nullable() 83 | }) 84 | } 85 | 86 | export function MyTypeFooArgsSchema(): myzod.Type { 87 | return myzod.object({ 88 | a: myzod.string().optional().nullable(), 89 | b: myzod.number(), 90 | c: myzod.boolean().optional().nullable(), 91 | d: myzod.number() 92 | }) 93 | } 94 | 95 | export function NamerSchema(): myzod.Type { 96 | return myzod.object({ 97 | name: myzod.string().optional().nullable() 98 | }) 99 | } 100 | 101 | export function PageInputSchema(): myzod.Type { 102 | return myzod.object({ 103 | attributes: myzod.array(myzod.lazy(() => AttributeInputSchema())).optional().nullable(), 104 | date: definedNonNullAnySchema.optional().nullable(), 105 | height: myzod.number(), 106 | id: myzod.string(), 107 | layout: myzod.lazy(() => LayoutInputSchema()), 108 | pageType: PageTypeSchema, 109 | postIDs: myzod.array(myzod.string()).optional().nullable(), 110 | show: myzod.boolean(), 111 | tags: myzod.array(myzod.string().nullable()).optional().nullable(), 112 | title: myzod.string(), 113 | width: myzod.number() 114 | }) 115 | } 116 | 117 | export function UserSchema(): myzod.Type { 118 | return myzod.object({ 119 | __typename: myzod.literal('User').optional(), 120 | createdAt: definedNonNullAnySchema.optional().nullable(), 121 | email: myzod.string().optional().nullable(), 122 | id: myzod.string().optional().nullable(), 123 | kind: UserKindSchema().optional().nullable(), 124 | name: myzod.string().optional().nullable(), 125 | password: myzod.string().optional().nullable(), 126 | updatedAt: definedNonNullAnySchema.optional().nullable() 127 | }) 128 | } 129 | 130 | export function UserKindSchema() { 131 | return myzod.union([AdminSchema(), GuestSchema()]) 132 | } 133 | -------------------------------------------------------------------------------- /example/test.graphql: -------------------------------------------------------------------------------- 1 | enum PageType { 2 | LP 3 | SERVICE 4 | RESTRICTED 5 | BASIC_AUTH 6 | } 7 | 8 | type Admin { 9 | lastModifiedAt: Date 10 | } 11 | 12 | type Guest { 13 | lastLoggedIn: Date 14 | } 15 | 16 | union UserKind = Admin | Guest 17 | 18 | type User implements Namer { 19 | id: ID 20 | name: String 21 | email: String 22 | password: String 23 | kind: UserKind 24 | createdAt: Date 25 | updatedAt: Date 26 | } 27 | 28 | interface Namer { 29 | name: String 30 | } 31 | 32 | input PageInput { 33 | id: ID! 34 | title: String! 35 | show: Boolean! 36 | width: Int! 37 | height: Float! 38 | layout: LayoutInput! 39 | tags: [String] 40 | attributes: [AttributeInput!] 41 | pageType: PageType! 42 | date: Date 43 | postIDs: [ID!] 44 | } 45 | 46 | input AttributeInput { 47 | key: String 48 | val: String 49 | } 50 | 51 | input LayoutInput { 52 | dropdown: DropDownComponentInput 53 | } 54 | 55 | input DropDownComponentInput { 56 | getEvent: EventInput! 57 | dropdownComponent: ComponentInput 58 | } 59 | 60 | enum ButtonComponentType { 61 | BUTTON 62 | SUBMIT 63 | } 64 | 65 | input ComponentInput { 66 | type: ButtonComponentType! 67 | name: String! 68 | event: EventInput 69 | child: ComponentInput 70 | childrens: [ComponentInput] 71 | } 72 | 73 | input EventInput { 74 | arguments: [EventArgumentInput!]! 75 | options: [EventOptionType!] 76 | } 77 | 78 | enum EventOptionType { 79 | RETRY 80 | RELOAD 81 | } 82 | 83 | input EventArgumentInput { 84 | name: String! @constraint(minLength: 5) 85 | value: String! @constraint(startsWith: "foo") 86 | } 87 | 88 | input HTTPInput { 89 | method: HTTPMethod 90 | url: URL! 91 | } 92 | 93 | enum HTTPMethod { 94 | GET 95 | POST 96 | } 97 | 98 | scalar Date 99 | scalar URL 100 | 101 | type MyType { 102 | foo(a: String, b: Int!, c: Boolean, d: Float!): String 103 | } 104 | 105 | # https://github.com/confuser/graphql-constraint-directive 106 | directive @constraint( 107 | # String constraints 108 | minLength: Int 109 | maxLength: Int 110 | startsWith: String 111 | endsWith: String 112 | contains: String 113 | notContains: String 114 | pattern: String 115 | format: String 116 | # Number constraints 117 | min: Float 118 | max: Float 119 | exclusiveMin: Float 120 | exclusiveMax: Float 121 | multipleOf: Float 122 | uniqueTypeName: String 123 | ) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION 124 | -------------------------------------------------------------------------------- /example/types.ts: -------------------------------------------------------------------------------- 1 | export type Maybe = T | null; 2 | export type InputMaybe = Maybe; 3 | export type Exact = { [K in keyof T]: T[K] }; 4 | export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; 5 | export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; 6 | export type MakeEmpty = { [_ in K]?: never }; 7 | export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; 8 | /** All built-in and custom scalars, mapped to their actual values */ 9 | export type Scalars = { 10 | ID: { input: string; output: string; } 11 | String: { input: string; output: string; } 12 | Boolean: { input: boolean; output: boolean; } 13 | Int: { input: number; output: number; } 14 | Float: { input: number; output: number; } 15 | Date: { input: any; output: any; } 16 | URL: { input: any; output: any; } 17 | }; 18 | 19 | export type Admin = { 20 | __typename?: 'Admin'; 21 | lastModifiedAt?: Maybe; 22 | }; 23 | 24 | export type AttributeInput = { 25 | key?: InputMaybe; 26 | val?: InputMaybe; 27 | }; 28 | 29 | export enum ButtonComponentType { 30 | Button = 'BUTTON', 31 | Submit = 'SUBMIT' 32 | } 33 | 34 | export type ComponentInput = { 35 | child?: InputMaybe; 36 | childrens?: InputMaybe>>; 37 | event?: InputMaybe; 38 | name: Scalars['String']['input']; 39 | type: ButtonComponentType; 40 | }; 41 | 42 | export type DropDownComponentInput = { 43 | dropdownComponent?: InputMaybe; 44 | getEvent: EventInput; 45 | }; 46 | 47 | export type EventArgumentInput = { 48 | name: Scalars['String']['input']; 49 | value: Scalars['String']['input']; 50 | }; 51 | 52 | export type EventInput = { 53 | arguments: Array; 54 | options?: InputMaybe>; 55 | }; 56 | 57 | export enum EventOptionType { 58 | Reload = 'RELOAD', 59 | Retry = 'RETRY' 60 | } 61 | 62 | export type Guest = { 63 | __typename?: 'Guest'; 64 | lastLoggedIn?: Maybe; 65 | }; 66 | 67 | export type HttpInput = { 68 | method?: InputMaybe; 69 | url: Scalars['URL']['input']; 70 | }; 71 | 72 | export enum HttpMethod { 73 | Get = 'GET', 74 | Post = 'POST' 75 | } 76 | 77 | export type LayoutInput = { 78 | dropdown?: InputMaybe; 79 | }; 80 | 81 | export type MyType = { 82 | __typename?: 'MyType'; 83 | foo?: Maybe; 84 | }; 85 | 86 | 87 | export type MyTypeFooArgs = { 88 | a?: InputMaybe; 89 | b: Scalars['Int']['input']; 90 | c?: InputMaybe; 91 | d: Scalars['Float']['input']; 92 | }; 93 | 94 | export type Namer = { 95 | name?: Maybe; 96 | }; 97 | 98 | export type PageInput = { 99 | attributes?: InputMaybe>; 100 | date?: InputMaybe; 101 | height: Scalars['Float']['input']; 102 | id: Scalars['ID']['input']; 103 | layout: LayoutInput; 104 | pageType: PageType; 105 | postIDs?: InputMaybe>; 106 | show: Scalars['Boolean']['input']; 107 | tags?: InputMaybe>>; 108 | title: Scalars['String']['input']; 109 | width: Scalars['Int']['input']; 110 | }; 111 | 112 | export enum PageType { 113 | BasicAuth = 'BASIC_AUTH', 114 | Lp = 'LP', 115 | Restricted = 'RESTRICTED', 116 | Service = 'SERVICE' 117 | } 118 | 119 | export type User = Namer & { 120 | __typename?: 'User'; 121 | createdAt?: Maybe; 122 | email?: Maybe; 123 | id?: Maybe; 124 | kind?: Maybe; 125 | name?: Maybe; 126 | password?: Maybe; 127 | updatedAt?: Maybe; 128 | }; 129 | 130 | export type UserKind = Admin | Guest; 131 | -------------------------------------------------------------------------------- /example/valibot/schemas.ts: -------------------------------------------------------------------------------- 1 | import * as v from 'valibot' 2 | import { Admin, AttributeInput, ButtonComponentType, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, Guest, HttpInput, HttpMethod, LayoutInput, MyType, MyTypeFooArgs, Namer, PageInput, PageType, User } from '../types' 3 | 4 | export const ButtonComponentTypeSchema = v.enum_(ButtonComponentType); 5 | 6 | export const EventOptionTypeSchema = v.enum_(EventOptionType); 7 | 8 | export const HttpMethodSchema = v.enum_(HttpMethod); 9 | 10 | export const PageTypeSchema = v.enum_(PageType); 11 | 12 | export function AdminSchema(): v.GenericSchema { 13 | return v.object({ 14 | __typename: v.optional(v.literal('Admin')), 15 | lastModifiedAt: v.nullish(v.any()) 16 | }) 17 | } 18 | 19 | export function AttributeInputSchema(): v.GenericSchema { 20 | return v.object({ 21 | key: v.nullish(v.string()), 22 | val: v.nullish(v.string()) 23 | }) 24 | } 25 | 26 | export function ComponentInputSchema(): v.GenericSchema { 27 | return v.object({ 28 | child: v.lazy(() => v.nullish(ComponentInputSchema())), 29 | childrens: v.nullish(v.array(v.lazy(() => v.nullable(ComponentInputSchema())))), 30 | event: v.lazy(() => v.nullish(EventInputSchema())), 31 | name: v.string(), 32 | type: ButtonComponentTypeSchema 33 | }) 34 | } 35 | 36 | export function DropDownComponentInputSchema(): v.GenericSchema { 37 | return v.object({ 38 | dropdownComponent: v.lazy(() => v.nullish(ComponentInputSchema())), 39 | getEvent: v.lazy(() => EventInputSchema()) 40 | }) 41 | } 42 | 43 | export function EventArgumentInputSchema(): v.GenericSchema { 44 | return v.object({ 45 | name: v.pipe(v.string(), v.minLength(5)), 46 | value: v.pipe(v.string(), v.regex(/^foo/, "message")) 47 | }) 48 | } 49 | 50 | export function EventInputSchema(): v.GenericSchema { 51 | return v.object({ 52 | arguments: v.array(v.lazy(() => EventArgumentInputSchema())), 53 | options: v.nullish(v.array(EventOptionTypeSchema)) 54 | }) 55 | } 56 | 57 | export function GuestSchema(): v.GenericSchema { 58 | return v.object({ 59 | __typename: v.optional(v.literal('Guest')), 60 | lastLoggedIn: v.nullish(v.any()) 61 | }) 62 | } 63 | 64 | export function HttpInputSchema(): v.GenericSchema { 65 | return v.object({ 66 | method: v.nullish(HttpMethodSchema), 67 | url: v.any() 68 | }) 69 | } 70 | 71 | export function LayoutInputSchema(): v.GenericSchema { 72 | return v.object({ 73 | dropdown: v.lazy(() => v.nullish(DropDownComponentInputSchema())) 74 | }) 75 | } 76 | 77 | export function MyTypeSchema(): v.GenericSchema { 78 | return v.object({ 79 | __typename: v.optional(v.literal('MyType')), 80 | foo: v.nullish(v.string()) 81 | }) 82 | } 83 | 84 | export function MyTypeFooArgsSchema(): v.GenericSchema { 85 | return v.object({ 86 | a: v.nullish(v.string()), 87 | b: v.number(), 88 | c: v.nullish(v.boolean()), 89 | d: v.number() 90 | }) 91 | } 92 | 93 | export function NamerSchema(): v.GenericSchema { 94 | return v.object({ 95 | name: v.nullish(v.string()) 96 | }) 97 | } 98 | 99 | export function PageInputSchema(): v.GenericSchema { 100 | return v.object({ 101 | attributes: v.nullish(v.array(v.lazy(() => AttributeInputSchema()))), 102 | date: v.nullish(v.any()), 103 | height: v.number(), 104 | id: v.string(), 105 | layout: v.lazy(() => LayoutInputSchema()), 106 | pageType: PageTypeSchema, 107 | postIDs: v.nullish(v.array(v.string())), 108 | show: v.boolean(), 109 | tags: v.nullish(v.array(v.nullable(v.string()))), 110 | title: v.string(), 111 | width: v.number() 112 | }) 113 | } 114 | 115 | export function UserSchema(): v.GenericSchema { 116 | return v.object({ 117 | __typename: v.optional(v.literal('User')), 118 | createdAt: v.nullish(v.any()), 119 | email: v.nullish(v.string()), 120 | id: v.nullish(v.string()), 121 | kind: v.nullish(UserKindSchema()), 122 | name: v.nullish(v.string()), 123 | password: v.nullish(v.string()), 124 | updatedAt: v.nullish(v.any()) 125 | }) 126 | } 127 | 128 | export function UserKindSchema() { 129 | return v.union([AdminSchema(), GuestSchema()]) 130 | } 131 | -------------------------------------------------------------------------------- /example/yup/README.md: -------------------------------------------------------------------------------- 1 | # Tips for yup schema 2 | 3 | ## How to overwrite generated schema? 4 | 5 | You can use yup [shape API](https://github.com/jquense/yup#objectshapefields-object-nosortedges-arraystring-string-schema). 6 | 7 | ```ts 8 | const AttributeInputSchemaWithUUID = AttributeInputSchema().shape({ 9 | key: z.string().uuid(), 10 | }); 11 | ``` -------------------------------------------------------------------------------- /example/yup/schemas.ts: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup' 2 | import { Admin, AttributeInput, ButtonComponentType, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, Guest, HttpInput, HttpMethod, LayoutInput, MyType, MyTypeFooArgs, Namer, PageInput, PageType, User, UserKind } from '../types' 3 | 4 | export const ButtonComponentTypeSchema = yup.string().oneOf(Object.values(ButtonComponentType)).defined(); 5 | 6 | export const EventOptionTypeSchema = yup.string().oneOf(Object.values(EventOptionType)).defined(); 7 | 8 | export const HttpMethodSchema = yup.string().oneOf(Object.values(HttpMethod)).defined(); 9 | 10 | export const PageTypeSchema = yup.string().oneOf(Object.values(PageType)).defined(); 11 | 12 | function union(...schemas: ReadonlyArray>): yup.MixedSchema { 13 | return yup.mixed().test({ 14 | test: (value) => schemas.some((schema) => schema.isValidSync(value)) 15 | }).defined() 16 | } 17 | 18 | export function AdminSchema(): yup.ObjectSchema { 19 | return yup.object({ 20 | __typename: yup.string<'Admin'>().optional(), 21 | lastModifiedAt: yup.mixed().nullable().optional() 22 | }) 23 | } 24 | 25 | export function AttributeInputSchema(): yup.ObjectSchema { 26 | return yup.object({ 27 | key: yup.string().defined().nullable().optional(), 28 | val: yup.string().defined().nullable().optional() 29 | }) 30 | } 31 | 32 | export function ComponentInputSchema(): yup.ObjectSchema { 33 | return yup.object({ 34 | child: yup.lazy(() => ComponentInputSchema()).optional(), 35 | childrens: yup.array(yup.lazy(() => ComponentInputSchema())).defined().nullable().optional(), 36 | event: yup.lazy(() => EventInputSchema()).optional(), 37 | name: yup.string().defined().nonNullable(), 38 | type: ButtonComponentTypeSchema.nonNullable() 39 | }) 40 | } 41 | 42 | export function DropDownComponentInputSchema(): yup.ObjectSchema { 43 | return yup.object({ 44 | dropdownComponent: yup.lazy(() => ComponentInputSchema()).optional(), 45 | getEvent: yup.lazy(() => EventInputSchema().nonNullable()) 46 | }) 47 | } 48 | 49 | export function EventArgumentInputSchema(): yup.ObjectSchema { 50 | return yup.object({ 51 | name: yup.string().defined().nonNullable().min(5), 52 | value: yup.string().defined().nonNullable().matches(/^foo/) 53 | }) 54 | } 55 | 56 | export function EventInputSchema(): yup.ObjectSchema { 57 | return yup.object({ 58 | arguments: yup.array(yup.lazy(() => EventArgumentInputSchema().nonNullable())).defined(), 59 | options: yup.array(EventOptionTypeSchema.nonNullable()).defined().nullable().optional() 60 | }) 61 | } 62 | 63 | export function GuestSchema(): yup.ObjectSchema { 64 | return yup.object({ 65 | __typename: yup.string<'Guest'>().optional(), 66 | lastLoggedIn: yup.mixed().nullable().optional() 67 | }) 68 | } 69 | 70 | export function HttpInputSchema(): yup.ObjectSchema { 71 | return yup.object({ 72 | method: HttpMethodSchema.nullable().optional(), 73 | url: yup.mixed().nonNullable() 74 | }) 75 | } 76 | 77 | export function LayoutInputSchema(): yup.ObjectSchema { 78 | return yup.object({ 79 | dropdown: yup.lazy(() => DropDownComponentInputSchema()).optional() 80 | }) 81 | } 82 | 83 | export function MyTypeSchema(): yup.ObjectSchema { 84 | return yup.object({ 85 | __typename: yup.string<'MyType'>().optional(), 86 | foo: yup.string().defined().nullable().optional() 87 | }) 88 | } 89 | 90 | export function MyTypeFooArgsSchema(): yup.ObjectSchema { 91 | return yup.object({ 92 | a: yup.string().defined().nullable().optional(), 93 | b: yup.number().defined().nonNullable(), 94 | c: yup.boolean().defined().nullable().optional(), 95 | d: yup.number().defined().nonNullable() 96 | }) 97 | } 98 | 99 | export function NamerSchema(): yup.ObjectSchema { 100 | return yup.object({ 101 | name: yup.string().defined().nullable().optional() 102 | }) 103 | } 104 | 105 | export function PageInputSchema(): yup.ObjectSchema { 106 | return yup.object({ 107 | attributes: yup.array(yup.lazy(() => AttributeInputSchema().nonNullable())).defined().nullable().optional(), 108 | date: yup.mixed().nullable().optional(), 109 | height: yup.number().defined().nonNullable(), 110 | id: yup.string().defined().nonNullable(), 111 | layout: yup.lazy(() => LayoutInputSchema().nonNullable()), 112 | pageType: PageTypeSchema.nonNullable(), 113 | postIDs: yup.array(yup.string().defined().nonNullable()).defined().nullable().optional(), 114 | show: yup.boolean().defined().nonNullable(), 115 | tags: yup.array(yup.string().defined().nullable()).defined().nullable().optional(), 116 | title: yup.string().defined().nonNullable(), 117 | width: yup.number().defined().nonNullable() 118 | }) 119 | } 120 | 121 | export function UserSchema(): yup.ObjectSchema { 122 | return yup.object({ 123 | __typename: yup.string<'User'>().optional(), 124 | createdAt: yup.mixed().nullable().optional(), 125 | email: yup.string().defined().nullable().optional(), 126 | id: yup.string().defined().nullable().optional(), 127 | kind: UserKindSchema().nullable().optional(), 128 | name: yup.string().defined().nullable().optional(), 129 | password: yup.string().defined().nullable().optional(), 130 | updatedAt: yup.mixed().nullable().optional() 131 | }) 132 | } 133 | 134 | export function UserKindSchema(): yup.MixedSchema { 135 | return union(AdminSchema(), GuestSchema()) 136 | } 137 | -------------------------------------------------------------------------------- /example/zod/README.md: -------------------------------------------------------------------------------- 1 | # Tips for zod schema 2 | 3 | ## How to overwrite generated schema? 4 | 5 | You can use zod [extend API](https://github.com/colinhacks/zod#extend). 6 | 7 | ```ts 8 | const AttributeInputSchemaWithCUID = AttributeInputSchema().extend({ 9 | key: z.string().cuid(), 10 | }); 11 | ``` 12 | 13 | ## Apply input validator via ts decorator 14 | 15 | Validate the input object via typescript decorators when implementing resolvers. See: #190 16 | 17 | ### Usage 18 | 19 | ```ts 20 | class Mutation { 21 | @validateInput(SignupInputSchema) 22 | async signup( 23 | _root: Record, 24 | { input: { email, password } }: MutationSignupArgs, 25 | context: Context 26 | ): Promise { 27 | // The input here is automatically valid to adhere to SignupInputSchema 28 | } 29 | } 30 | ``` 31 | 32 | ### Implementation: 33 | 34 | ```ts 35 | type ZodResolver> = ResolverFn< 36 | any, 37 | any, 38 | any, 39 | { input: TypeOf } 40 | > 41 | 42 | /** 43 | * Method decorator that validates the argument of the target function against the given schema. 44 | * 45 | * @export 46 | * @template T The type of the zod schema. 47 | * @param {T} arg The zod schema used for the validation. 48 | * @return {MethodDecorator} A {@link MethodDecorator}. 49 | */ 50 | export function validateInput( 51 | arg: T | (() => T) 52 | ): MethodDecorator> { 53 | return function (_target, _propertyKey, descriptor) { 54 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 55 | const originalMethod = descriptor.value! 56 | // @ts-expect-error: should be fine 57 | descriptor.value = function (root, { input }, context, info) { 58 | const schema = typeof arg === 'function' ? arg() : arg 59 | const result = schema.safeParse(input) 60 | 61 | if (result.success) { 62 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 63 | return originalMethod.call( 64 | this, 65 | root, 66 | { input: result.data }, 67 | context, 68 | info 69 | ) 70 | } else { 71 | return { problems: result.error.issues } 72 | } 73 | } 74 | return descriptor 75 | } 76 | } 77 | ``` -------------------------------------------------------------------------------- /example/zod/schemas.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import { Admin, AttributeInput, ButtonComponentType, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, Guest, HttpInput, HttpMethod, LayoutInput, MyType, MyTypeFooArgs, Namer, PageInput, PageType, User } from '../types' 3 | 4 | type Properties = Required<{ 5 | [K in keyof T]: z.ZodType; 6 | }>; 7 | 8 | type definedNonNullAny = {}; 9 | 10 | export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; 11 | 12 | export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); 13 | 14 | export const ButtonComponentTypeSchema = z.nativeEnum(ButtonComponentType); 15 | 16 | export const EventOptionTypeSchema = z.nativeEnum(EventOptionType); 17 | 18 | export const HttpMethodSchema = z.nativeEnum(HttpMethod); 19 | 20 | export const PageTypeSchema = z.nativeEnum(PageType); 21 | 22 | export function AdminSchema(): z.ZodObject> { 23 | return z.object({ 24 | __typename: z.literal('Admin').optional(), 25 | lastModifiedAt: definedNonNullAnySchema.nullish() 26 | }) 27 | } 28 | 29 | export function AttributeInputSchema(): z.ZodObject> { 30 | return z.object({ 31 | key: z.string().nullish(), 32 | val: z.string().nullish() 33 | }) 34 | } 35 | 36 | export function ComponentInputSchema(): z.ZodObject> { 37 | return z.object({ 38 | child: z.lazy(() => ComponentInputSchema().nullish()), 39 | childrens: z.array(z.lazy(() => ComponentInputSchema().nullable())).nullish(), 40 | event: z.lazy(() => EventInputSchema().nullish()), 41 | name: z.string(), 42 | type: ButtonComponentTypeSchema 43 | }) 44 | } 45 | 46 | export function DropDownComponentInputSchema(): z.ZodObject> { 47 | return z.object({ 48 | dropdownComponent: z.lazy(() => ComponentInputSchema().nullish()), 49 | getEvent: z.lazy(() => EventInputSchema()) 50 | }) 51 | } 52 | 53 | export function EventArgumentInputSchema(): z.ZodObject> { 54 | return z.object({ 55 | name: z.string().min(5), 56 | value: z.string().regex(/^foo/, "message") 57 | }) 58 | } 59 | 60 | export function EventInputSchema(): z.ZodObject> { 61 | return z.object({ 62 | arguments: z.array(z.lazy(() => EventArgumentInputSchema())), 63 | options: z.array(EventOptionTypeSchema).nullish() 64 | }) 65 | } 66 | 67 | export function GuestSchema(): z.ZodObject> { 68 | return z.object({ 69 | __typename: z.literal('Guest').optional(), 70 | lastLoggedIn: definedNonNullAnySchema.nullish() 71 | }) 72 | } 73 | 74 | export function HttpInputSchema(): z.ZodObject> { 75 | return z.object({ 76 | method: HttpMethodSchema.nullish(), 77 | url: definedNonNullAnySchema 78 | }) 79 | } 80 | 81 | export function LayoutInputSchema(): z.ZodObject> { 82 | return z.object({ 83 | dropdown: z.lazy(() => DropDownComponentInputSchema().nullish()) 84 | }) 85 | } 86 | 87 | export function MyTypeSchema(): z.ZodObject> { 88 | return z.object({ 89 | __typename: z.literal('MyType').optional(), 90 | foo: z.string().nullish() 91 | }) 92 | } 93 | 94 | export function MyTypeFooArgsSchema(): z.ZodObject> { 95 | return z.object({ 96 | a: z.string().nullish(), 97 | b: z.number(), 98 | c: z.boolean().nullish(), 99 | d: z.number() 100 | }) 101 | } 102 | 103 | export function NamerSchema(): z.ZodObject> { 104 | return z.object({ 105 | name: z.string().nullish() 106 | }) 107 | } 108 | 109 | export function PageInputSchema(): z.ZodObject> { 110 | return z.object({ 111 | attributes: z.array(z.lazy(() => AttributeInputSchema())).nullish(), 112 | date: definedNonNullAnySchema.nullish(), 113 | height: z.number(), 114 | id: z.string(), 115 | layout: z.lazy(() => LayoutInputSchema()), 116 | pageType: PageTypeSchema, 117 | postIDs: z.array(z.string()).nullish(), 118 | show: z.boolean(), 119 | tags: z.array(z.string().nullable()).nullish(), 120 | title: z.string(), 121 | width: z.number() 122 | }) 123 | } 124 | 125 | export function UserSchema(): z.ZodObject> { 126 | return z.object({ 127 | __typename: z.literal('User').optional(), 128 | createdAt: definedNonNullAnySchema.nullish(), 129 | email: z.string().nullish(), 130 | id: z.string().nullish(), 131 | kind: UserKindSchema().nullish(), 132 | name: z.string().nullish(), 133 | password: z.string().nullish(), 134 | updatedAt: definedNonNullAnySchema.nullish() 135 | }) 136 | } 137 | 138 | export function UserKindSchema() { 139 | return z.union([AdminSchema(), GuestSchema()]) 140 | } 141 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-codegen-typescript-validation-schema", 3 | "type": "module", 4 | "version": "0.17.1", 5 | "packageManager": "pnpm@10.11.1", 6 | "description": "GraphQL Code Generator plugin to generate form validation schema from your GraphQL schema", 7 | "respository": { 8 | "type": "git", 9 | "url": "https://github.com/Code-Hex/graphql-codegen-typescript-validation-schema.git" 10 | }, 11 | "author": "codehex", 12 | "license": "MIT", 13 | "bugs": { 14 | "url": "https://github.com/Code-Hex/graphql-codegen-typescript-validation-schema/issues" 15 | }, 16 | "keywords": [ 17 | "gql", 18 | "generator", 19 | "yup", 20 | "zod", 21 | "code", 22 | "types", 23 | "graphql", 24 | "codegen", 25 | "apollo", 26 | "node", 27 | "types", 28 | "typings" 29 | ], 30 | "exports": { 31 | ".": { 32 | "import": { 33 | "types": "./dist/types/index.d.ts", 34 | "default": "./dist/esm/index.js" 35 | }, 36 | "require": { 37 | "types": "./dist/types/index.d.ts", 38 | "default": "./dist/cjs/index.js" 39 | }, 40 | "default": { 41 | "types": "./dist/types/index.d.ts", 42 | "default": "./dist/esm/index.js" 43 | } 44 | }, 45 | "./package.json": "./package.json" 46 | }, 47 | "main": "dist/cjs/index.js", 48 | "module": "dist/esm/index.js", 49 | "typings": "dist/types/index.d.ts", 50 | "typescript": { 51 | "definition": "dist/types/index.d.ts" 52 | }, 53 | "files": [ 54 | "!dist/**/*.tsbuildinfo", 55 | "LICENSE", 56 | "README.md", 57 | "dist/**/*.{js,ts,json}" 58 | ], 59 | "scripts": { 60 | "type-check": "tsc --noEmit", 61 | "type-check:yup": "tsc --strict --skipLibCheck --noEmit example/yup/schemas.ts", 62 | "type-check:zod": "tsc --strict --skipLibCheck --noEmit example/zod/schemas.ts", 63 | "type-check:myzod": "tsc --strict --skipLibCheck --noEmit example/myzod/schemas.ts", 64 | "type-check:valibot": "tsc --strict --skipLibCheck --noEmit example/valibot/schemas.ts", 65 | "test": "vitest run", 66 | "build": "run-p build:*", 67 | "build:cjs": "tsc -p tsconfig.cjs.json && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json", 68 | "build:esm": "tsc -p tsconfig.esm.json && echo '{\"type\":\"module\"}' > dist/esm/package.json", 69 | "build:types": "tsc -p tsconfig.types.json", 70 | "lint": "eslint .", 71 | "lint-fix": "eslint . --fix", 72 | "generate": "run-p build:* && graphql-codegen", 73 | "generate:esm": "run-p build:* && graphql-codegen-esm", 74 | "prepublish": "run-p build:*" 75 | }, 76 | "peerDependencies": { 77 | "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" 78 | }, 79 | "dependencies": { 80 | "@graphql-codegen/plugin-helpers": "^5.0.0", 81 | "@graphql-codegen/schema-ast": "4.1.0", 82 | "@graphql-codegen/visitor-plugin-common": "^5.0.0", 83 | "@graphql-tools/utils": "^10.0.0", 84 | "graphlib": "^2.1.8", 85 | "graphql": "^16.6.0" 86 | }, 87 | "devDependencies": { 88 | "@antfu/eslint-config": "^4.0.0", 89 | "@graphql-codegen/cli": "5.0.7", 90 | "@graphql-codegen/typescript": "^4.0.0", 91 | "@tsconfig/recommended": "1.0.8", 92 | "@types/graphlib": "^2.1.8", 93 | "@types/node": "^22.0.0", 94 | "eslint": "9.28.0", 95 | "jest": "29.7.0", 96 | "myzod": "1.12.1", 97 | "npm-run-all2": "8.0.4", 98 | "ts-dedent": "^2.2.0", 99 | "ts-jest": "29.3.4", 100 | "typescript": "5.8.3", 101 | "valibot": "1.1.0", 102 | "vitest": "^3.0.0", 103 | "yup": "1.6.1", 104 | "zod": "3.25.56" 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import type { TypeScriptPluginConfig } from '@graphql-codegen/typescript'; 2 | import type { NamingConventionMap } from '@graphql-codegen/visitor-plugin-common'; 3 | 4 | export type ValidationSchema = 'yup' | 'zod' | 'myzod' | 'valibot'; 5 | export type ValidationSchemaExportType = 'function' | 'const'; 6 | 7 | export interface DirectiveConfig { 8 | [directive: string]: { 9 | [argument: string]: string | string[] | DirectiveObjectArguments 10 | } 11 | } 12 | 13 | export interface DirectiveObjectArguments { 14 | [matched: string]: string | string[] 15 | } 16 | 17 | interface ScalarSchemas { 18 | [name: string]: string 19 | } 20 | 21 | export interface ValidationSchemaPluginConfig extends TypeScriptPluginConfig { 22 | /** 23 | * @description specify generate schema 24 | * @default yup 25 | * 26 | * @exampleMarkdown 27 | * ```yml 28 | * generates: 29 | * path/to/file.ts: 30 | * plugins: 31 | * - typescript 32 | * - graphql-codegen-validation-schema 33 | * config: 34 | * schema: yup 35 | * ``` 36 | */ 37 | schema?: ValidationSchema 38 | /** 39 | * @description import types from generated typescript type path 40 | * if not given, omit import statement. 41 | * 42 | * @exampleMarkdown 43 | * ```yml 44 | * generates: 45 | * path/to/types.ts: 46 | * plugins: 47 | * - typescript 48 | * path/to/schemas.ts: 49 | * plugins: 50 | * - graphql-codegen-validation-schema 51 | * config: 52 | * schema: yup 53 | * importFrom: ./path/to/types 54 | * ``` 55 | */ 56 | importFrom?: string 57 | /** 58 | * @description If defined, will use named imports from the specified module (defined in `importFrom`) 59 | * rather than individual imports for each type. 60 | * 61 | * @exampleMarkdown 62 | * ```yml 63 | * generates: 64 | * path/to/types.ts: 65 | * plugins: 66 | * - typescript 67 | * path/to/schemas.ts: 68 | * plugins: 69 | * - graphql-codegen-validation-schema 70 | * config: 71 | * schema: yup 72 | * importFrom: ./path/to/types 73 | * schemaNamespacedImportName: types 74 | * ``` 75 | */ 76 | schemaNamespacedImportName?: string 77 | /** 78 | * @description Will use `import type {}` rather than `import {}` when importing generated typescript types. 79 | * This gives compatibility with TypeScript's "importsNotUsedAsValues": "error" option 80 | * Should used in conjunction with `importFrom` option. 81 | * @default false 82 | * 83 | * @exampleMarkdown 84 | * ```yml 85 | * generates: 86 | * path/to/types.ts: 87 | * plugins: 88 | * - typescript 89 | * path/to/schemas.ts: 90 | * plugins: 91 | * - graphql-codegen-validation-schema 92 | * config: 93 | * schema: yup 94 | * importFrom: ./path/to/types 95 | * useTypeImports: true 96 | * ``` 97 | */ 98 | useTypeImports?: boolean 99 | /** 100 | * @description Prefixes all import types from generated typescript type. 101 | * @default "" 102 | * 103 | * @exampleMarkdown 104 | * ```yml 105 | * generates: 106 | * path/to/types.ts: 107 | * plugins: 108 | * - typescript 109 | * path/to/schemas.ts: 110 | * plugins: 111 | * - graphql-codegen-validation-schema 112 | * config: 113 | * typesPrefix: I 114 | * importFrom: ./path/to/types 115 | * ``` 116 | */ 117 | typesPrefix?: string 118 | /** 119 | * @description Suffixes all import types from generated typescript type. 120 | * @default "" 121 | * 122 | * @exampleMarkdown 123 | * ```yml 124 | * generates: 125 | * path/to/types.ts: 126 | * plugins: 127 | * - typescript 128 | * path/to/schemas.ts: 129 | * plugins: 130 | * - graphql-codegen-validation-schema 131 | * config: 132 | * typesSuffix: I 133 | * importFrom: ./path/to/types 134 | * ``` 135 | */ 136 | typesSuffix?: string 137 | /** 138 | * @description Generates validation schema for enum as TypeScript `type` 139 | * @default false 140 | * 141 | * @exampleMarkdown 142 | * ```yml 143 | * generates: 144 | * path/to/file.ts: 145 | * plugins: 146 | * - graphql-codegen-validation-schema 147 | * config: 148 | * enumsAsTypes: true 149 | * ``` 150 | * 151 | * ```yml 152 | * generates: 153 | * path/to/file.ts: 154 | * plugins: 155 | * - typescript 156 | * - graphql-codegen-validation-schema 157 | * config: 158 | * enumsAsTypes: true 159 | * ``` 160 | */ 161 | enumsAsTypes?: boolean 162 | /** 163 | * @description Generates validation string schema as do not allow empty characters by default. 164 | * @default false 165 | * 166 | * @exampleMarkdown 167 | * ```yml 168 | * generates: 169 | * path/to/file.ts: 170 | * plugins: 171 | * - graphql-codegen-validation-schema 172 | * config: 173 | * notAllowEmptyString: true 174 | * ``` 175 | */ 176 | notAllowEmptyString?: boolean 177 | /** 178 | * @description Extends or overrides validation schema for the built-in scalars and custom GraphQL scalars. 179 | * 180 | * @exampleMarkdown 181 | * ```yml 182 | * config: 183 | * schema: yup 184 | * scalarSchemas: 185 | * Date: yup.date() 186 | * Email: yup.string().email() 187 | * ``` 188 | * 189 | * @exampleMarkdown 190 | * ```yml 191 | * config: 192 | * schema: zod 193 | * scalarSchemas: 194 | * Date: z.date() 195 | * Email: z.string().email() 196 | * ``` 197 | */ 198 | scalarSchemas?: ScalarSchemas 199 | /** 200 | * @description Fallback scalar type for undefined scalar types in the schema not found in `scalarSchemas`. 201 | * 202 | * @exampleMarkdown 203 | * ```yml 204 | * config: 205 | * schema: yup 206 | * defaultScalarSchema: yup.unknown() 207 | * ``` 208 | * 209 | * @exampleMarkdown 210 | * ```yml 211 | * config: 212 | * schema: zod 213 | * defaultScalarSchema: z.unknown() 214 | * ``` 215 | */ 216 | defaultScalarTypeSchema?: string 217 | /** 218 | * @description Generates validation schema with GraphQL type objects. 219 | * but excludes "Query", "Mutation", "Subscription" objects. 220 | * 221 | * @exampleMarkdown 222 | * ```yml 223 | * generates: 224 | * path/to/types.ts: 225 | * plugins: 226 | * - typescript 227 | * path/to/schemas.ts: 228 | * plugins: 229 | * - graphql-codegen-validation-schema 230 | * config: 231 | * schema: yup 232 | * withObjectType: true 233 | * ``` 234 | */ 235 | withObjectType?: boolean 236 | /** 237 | * @description Specify validation schema export type. 238 | * @default function 239 | * 240 | * @exampleMarkdown 241 | * ```yml 242 | * generates: 243 | * path/to/file.ts: 244 | * plugins: 245 | * - typescript 246 | * - graphql-codegen-validation-schema 247 | * config: 248 | * validationSchemaExportType: const 249 | * ``` 250 | */ 251 | validationSchemaExportType?: ValidationSchemaExportType 252 | /** 253 | * @description Uses the full path of the enum type as the default value instead of the stringified value. 254 | * @default false 255 | * 256 | * @exampleMarkdown 257 | * ```yml 258 | * generates: 259 | * path/to/file.ts: 260 | * plugins: 261 | * - typescript 262 | * - graphql-codegen-validation-schema 263 | * config: 264 | * useEnumTypeAsDefaultValue: true 265 | * ``` 266 | */ 267 | useEnumTypeAsDefaultValue?: boolean 268 | /** 269 | * @description Uses the full path of the enum type as the default value instead of the stringified value. 270 | * @default { enumValues: "change-case-all#pascalCase" } 271 | * @link https://the-guild.dev/graphql/codegen/docs/config-reference/naming-convention 272 | * 273 | * Note: This option has not been tested with `namingConvention.transformUnderscore` and `namingConvention.typeNames` options, 274 | * and may not work as expected. 275 | * 276 | * @exampleMarkdown 277 | * ```yml 278 | * generates: 279 | * path/to/file.ts: 280 | * plugins: 281 | * - typescript 282 | * - graphql-codegen-validation-schema 283 | * config: 284 | * namingConvention: 285 | * enumValues: change-case-all#pascalCase 286 | * ``` 287 | */ 288 | namingConvention?: NamingConventionMap 289 | /** 290 | * @description Generates validation schema with more API based on directive schema. 291 | * @exampleMarkdown 292 | * ```yml 293 | * generates: 294 | * path/to/file.ts: 295 | * plugins: 296 | * - graphql-codegen-validation-schema 297 | * config: 298 | * schema: yup 299 | * directives: 300 | * required: 301 | * msg: required 302 | * # This is example using constraint directive. 303 | * # see: https://github.com/confuser/graphql-constraint-directive 304 | * constraint: 305 | * minLength: min # same as ['min', '$1'] 306 | * maxLength: max 307 | * startsWith: ["matches", "/^$1/"] 308 | * endsWith: ["matches", "/$1$/"] 309 | * contains: ["matches", "/$1/"] 310 | * notContains: ["matches", "/^((?!$1).)*$/"] 311 | * pattern: ["matches", "/$1/"] 312 | * format: 313 | * # For example, `@constraint(format: "uri")`. this case $1 will be "uri". 314 | * # Therefore the generator generates yup schema `.url()` followed by `uri: 'url'` 315 | * # If $1 does not match anywhere, the generator will ignore. 316 | * uri: url 317 | * email: email 318 | * uuid: uuid 319 | * # yup does not have `ipv4` API. If you want to add this, 320 | * # you need to add the logic using `yup.addMethod`. 321 | * # see: https://github.com/jquense/yup#addmethodschematype-schema-name-string-method--schema-void 322 | * ipv4: ipv4 323 | * min: ["min", "$1 - 1"] 324 | * max: ["max", "$1 + 1"] 325 | * exclusiveMin: min 326 | * exclusiveMax: max 327 | * ``` 328 | */ 329 | directives?: DirectiveConfig 330 | } 331 | -------------------------------------------------------------------------------- /src/directive.ts: -------------------------------------------------------------------------------- 1 | import type { ConstArgumentNode, ConstDirectiveNode, ConstValueNode } from 'graphql'; 2 | import type { DirectiveConfig, DirectiveObjectArguments } from './config.js'; 3 | import { Kind, valueFromASTUntyped } from 'graphql'; 4 | 5 | import { isConvertableRegexp } from './regexp.js'; 6 | 7 | export interface FormattedDirectiveConfig { 8 | [directive: string]: FormattedDirectiveArguments 9 | } 10 | 11 | export interface FormattedDirectiveArguments { 12 | [argument: string]: string[] | FormattedDirectiveObjectArguments | undefined 13 | } 14 | 15 | export interface FormattedDirectiveObjectArguments { 16 | [matched: string]: string[] | undefined 17 | } 18 | 19 | function isFormattedDirectiveObjectArguments(arg: FormattedDirectiveArguments[keyof FormattedDirectiveArguments]): arg is FormattedDirectiveObjectArguments { 20 | return arg !== undefined && !Array.isArray(arg) 21 | } 22 | 23 | // ```yml 24 | // directives: 25 | // required: 26 | // msg: required 27 | // constraint: 28 | // minLength: min 29 | // format: 30 | // uri: url 31 | // email: email 32 | // ``` 33 | // 34 | // This function convterts to like below 35 | // { 36 | // 'required': { 37 | // 'msg': ['required', '$1'], 38 | // }, 39 | // 'constraint': { 40 | // 'minLength': ['min', '$1'], 41 | // 'format': { 42 | // 'uri': ['url', '$2'], 43 | // 'email': ['email', '$2'], 44 | // } 45 | // } 46 | // } 47 | export function formatDirectiveConfig(config: DirectiveConfig): FormattedDirectiveConfig { 48 | return Object.fromEntries( 49 | Object.entries(config).map(([directive, arg]) => { 50 | const formatted = Object.fromEntries( 51 | Object.entries(arg).map(([arg, val]) => { 52 | if (Array.isArray(val)) 53 | return [arg, val]; 54 | 55 | if (typeof val === 'string') 56 | return [arg, [val, '$1']]; 57 | 58 | return [arg, formatDirectiveObjectArguments(val)]; 59 | }), 60 | ); 61 | return [directive, formatted]; 62 | }), 63 | ); 64 | } 65 | 66 | // ```yml 67 | // format: 68 | // # For example, `@constraint(format: "uri")`. this case $1 will be "uri". 69 | // # Therefore the generator generates yup schema `.url()` followed by `uri: 'url'` 70 | // # If $1 does not match anywhere, the generator will ignore. 71 | // uri: url 72 | // email: ["email", "$2"] 73 | // ``` 74 | // 75 | // This function convterts to like below 76 | // { 77 | // 'uri': ['url', '$2'], 78 | // 'email': ['email'], 79 | // } 80 | export function formatDirectiveObjectArguments(args: DirectiveObjectArguments): FormattedDirectiveObjectArguments { 81 | const formatted = Object.entries(args).map(([arg, val]) => { 82 | if (Array.isArray(val)) 83 | return [arg, val]; 84 | 85 | return [arg, [val, '$2']]; 86 | }); 87 | return Object.fromEntries(formatted); 88 | } 89 | 90 | // This function generates `.required("message").min(100).email()` 91 | // 92 | // config 93 | // { 94 | // 'required': { 95 | // 'msg': ['required', '$1'], 96 | // }, 97 | // 'constraint': { 98 | // 'minLength': ['min', '$1'], 99 | // 'format': { 100 | // 'uri': ['url', '$2'], 101 | // 'email': ['email', '$2'], 102 | // } 103 | // } 104 | // } 105 | // 106 | // GraphQL schema 107 | // ```graphql 108 | // input ExampleInput { 109 | // email: String! @required(msg: "message") @constraint(minLength: 100, format: "email") 110 | // } 111 | // ``` 112 | export function buildApi(config: FormattedDirectiveConfig, directives: ReadonlyArray): string { 113 | return directives 114 | .filter(directive => config[directive.name.value] !== undefined) 115 | .map((directive) => { 116 | const directiveName = directive.name.value; 117 | const argsConfig = config[directiveName]; 118 | return buildApiFromDirectiveArguments(argsConfig, directive.arguments ?? []); 119 | }) 120 | .join('') 121 | } 122 | 123 | // This function generates `[v.minLength(100), v.email()]` 124 | // NOTE: valibot's API is not a method chain, so it is prepared separately from buildApi. 125 | // 126 | // config 127 | // { 128 | // 'constraint': { 129 | // 'minLength': ['minLength', '$1'], 130 | // 'format': { 131 | // 'uri': ['url', '$2'], 132 | // 'email': ['email', '$2'], 133 | // } 134 | // } 135 | // } 136 | // 137 | // GraphQL schema 138 | // ```graphql 139 | // input ExampleInput { 140 | // email: String! @required(msg: "message") @constraint(minLength: 100, format: "email") 141 | // } 142 | // ``` 143 | // 144 | // FIXME: v.required() is not supported yet. v.required() is classified as `Methods` and must wrap the schema. ex) `v.required(v.object({...}))` 145 | export function buildApiForValibot(config: FormattedDirectiveConfig, directives: ReadonlyArray): string[] { 146 | return directives 147 | .filter(directive => config[directive.name.value] !== undefined) 148 | .map((directive) => { 149 | const directiveName = directive.name.value; 150 | const argsConfig = config[directiveName]; 151 | const apis = _buildApiFromDirectiveArguments(argsConfig, directive.arguments ?? []); 152 | return apis.map(api => `v${api}`); 153 | }) 154 | .flat() 155 | } 156 | 157 | function buildApiSchema(validationSchema: string[] | undefined, argValue: ConstValueNode): string { 158 | if (!validationSchema) 159 | return ''; 160 | 161 | const schemaApi = validationSchema[0]; 162 | const schemaApiArgs = validationSchema.slice(1).map((templateArg) => { 163 | const gqlSchemaArgs = apiArgsFromConstValueNode(argValue); 164 | return applyArgToApiSchemaTemplate(templateArg, gqlSchemaArgs); 165 | }); 166 | return `.${schemaApi}(${schemaApiArgs.join(', ')})`; 167 | } 168 | 169 | function buildApiFromDirectiveArguments(config: FormattedDirectiveArguments, args: ReadonlyArray): string { 170 | return _buildApiFromDirectiveArguments(config, args).join(''); 171 | } 172 | 173 | function _buildApiFromDirectiveArguments(config: FormattedDirectiveArguments, args: ReadonlyArray): string[] { 174 | return args 175 | .map((arg) => { 176 | const argName = arg.name.value; 177 | const validationSchema = config[argName]; 178 | if (isFormattedDirectiveObjectArguments(validationSchema)) 179 | return buildApiFromDirectiveObjectArguments(validationSchema, arg.value); 180 | 181 | return buildApiSchema(validationSchema, arg.value); 182 | }) 183 | } 184 | 185 | function buildApiFromDirectiveObjectArguments(config: FormattedDirectiveObjectArguments, argValue: ConstValueNode): string { 186 | if (argValue.kind !== Kind.STRING && argValue.kind !== Kind.ENUM) 187 | return ''; 188 | 189 | const validationSchema = config[argValue.value]; 190 | return buildApiSchema(validationSchema, argValue); 191 | } 192 | 193 | function applyArgToApiSchemaTemplate(template: string, apiArgs: any[]): string { 194 | const matches = template.matchAll(/\$(\d+)/g); 195 | for (const match of matches) { 196 | const placeholder = match[0]; // `$1` 197 | const idx = Number.parseInt(match[1], 10) - 1; // start with `1 - 1` 198 | const apiArg = apiArgs[idx]; 199 | if (apiArg === undefined) { 200 | template = template.replace(placeholder, ''); 201 | continue; 202 | } 203 | if (template === placeholder) 204 | return stringify(apiArg); 205 | 206 | template = template.replace(placeholder, apiArg); 207 | } 208 | if (template !== '') 209 | return stringify(template, true); 210 | 211 | return template; 212 | } 213 | 214 | function stringify(arg: any, quoteString?: boolean): string { 215 | if (Array.isArray(arg)) 216 | return arg.map(v => stringify(v, true)).join(','); 217 | 218 | if (typeof arg === 'string') { 219 | if (isConvertableRegexp(arg)) 220 | return arg; 221 | 222 | const v = tryEval(arg) 223 | if (v !== undefined) 224 | arg = v 225 | 226 | if (quoteString) 227 | return JSON.stringify(arg); 228 | } 229 | 230 | if (typeof arg === 'boolean' || typeof arg === 'number' || typeof arg === 'bigint' || arg === 'undefined' || arg === null) 231 | return `${arg}`; 232 | 233 | return JSON.stringify(arg); 234 | } 235 | 236 | function apiArgsFromConstValueNode(value: ConstValueNode): any[] { 237 | const val = valueFromASTUntyped(value); 238 | if (Array.isArray(val)) 239 | return val; 240 | 241 | return [val]; 242 | } 243 | 244 | function tryEval(maybeValidJavaScript: string): any | undefined { 245 | try { 246 | // eslint-disable-next-line no-eval 247 | return eval(maybeValidJavaScript) 248 | } 249 | catch { 250 | return undefined 251 | } 252 | } 253 | 254 | export const exportedForTesting = { 255 | applyArgToApiSchemaTemplate, 256 | buildApiFromDirectiveObjectArguments, 257 | buildApiFromDirectiveArguments, 258 | }; 259 | -------------------------------------------------------------------------------- /src/graphql.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ASTNode, 3 | DefinitionNode, 4 | DocumentNode, 5 | GraphQLSchema, 6 | InterfaceTypeDefinitionNode, 7 | ListTypeNode, 8 | NamedTypeNode, 9 | NameNode, 10 | NonNullTypeNode, 11 | ObjectTypeDefinitionNode, 12 | TypeNode, 13 | } from 'graphql'; 14 | import { Graph } from 'graphlib'; 15 | import { 16 | isSpecifiedScalarType, 17 | Kind, 18 | visit, 19 | } from 'graphql'; 20 | 21 | /** 22 | * Recursively unwraps a GraphQL type until it reaches the NamedType. 23 | * 24 | * Since a GraphQL type is defined as either a NamedTypeNode, ListTypeNode, or NonNullTypeNode, 25 | * this implementation safely recurses until the underlying NamedTypeNode is reached. 26 | */ 27 | function getNamedType(typeNode: TypeNode): NamedTypeNode { 28 | return typeNode.kind === Kind.NAMED_TYPE ? typeNode : getNamedType(typeNode.type); 29 | } 30 | 31 | export const isListType = (typ?: TypeNode): typ is ListTypeNode => typ?.kind === Kind.LIST_TYPE; 32 | export const isNonNullType = (typ?: TypeNode): typ is NonNullTypeNode => typ?.kind === Kind.NON_NULL_TYPE; 33 | export const isNamedType = (typ?: TypeNode): typ is NamedTypeNode => typ?.kind === Kind.NAMED_TYPE; 34 | 35 | export const isInput = (kind: string) => kind.includes('Input'); 36 | 37 | type ObjectTypeDefinitionFn = (node: ObjectTypeDefinitionNode) => any; 38 | type InterfaceTypeDefinitionFn = (node: InterfaceTypeDefinitionNode) => any; 39 | 40 | export function ObjectTypeDefinitionBuilder(useObjectTypes: boolean | undefined, callback: ObjectTypeDefinitionFn): ObjectTypeDefinitionFn | undefined { 41 | if (!useObjectTypes) 42 | return undefined; 43 | return (node) => { 44 | if (/^(Query|Mutation|Subscription)$/.test(node.name.value)) 45 | return; 46 | 47 | return callback(node); 48 | }; 49 | } 50 | 51 | export function InterfaceTypeDefinitionBuilder(useInterfaceTypes: boolean | undefined, callback: InterfaceTypeDefinitionFn): InterfaceTypeDefinitionFn | undefined { 52 | if (!useInterfaceTypes) 53 | return undefined; 54 | return (node) => { 55 | return callback(node); 56 | }; 57 | } 58 | 59 | export function topologicalSortAST(schema: GraphQLSchema, ast: DocumentNode): DocumentNode { 60 | const dependencyGraph = new Graph(); 61 | const targetKinds = [ 62 | Kind.OBJECT_TYPE_DEFINITION, 63 | Kind.INPUT_OBJECT_TYPE_DEFINITION, 64 | Kind.INTERFACE_TYPE_DEFINITION, 65 | Kind.SCALAR_TYPE_DEFINITION, 66 | Kind.ENUM_TYPE_DEFINITION, 67 | Kind.UNION_TYPE_DEFINITION, 68 | ]; 69 | 70 | visit(ast, { 71 | enter: (node) => { 72 | switch (node.kind) { 73 | case Kind.OBJECT_TYPE_DEFINITION: 74 | case Kind.INPUT_OBJECT_TYPE_DEFINITION: 75 | case Kind.INTERFACE_TYPE_DEFINITION: { 76 | const typeName = node.name.value; 77 | dependencyGraph.setNode(typeName); 78 | 79 | if (node.fields) { 80 | node.fields.forEach((field) => { 81 | // Unwrap the type 82 | const namedTypeNode = getNamedType(field.type); 83 | const dependency = namedTypeNode.name.value; 84 | const namedType = schema.getType(dependency); 85 | if ( 86 | namedType?.astNode?.kind === undefined 87 | || !targetKinds.includes(namedType.astNode.kind) 88 | ) { 89 | return; 90 | } 91 | 92 | if (!dependencyGraph.hasNode(dependency)) { 93 | dependencyGraph.setNode(dependency); 94 | } 95 | dependencyGraph.setEdge(typeName, dependency); 96 | }); 97 | } 98 | break; 99 | } 100 | case Kind.SCALAR_TYPE_DEFINITION: 101 | case Kind.ENUM_TYPE_DEFINITION: { 102 | dependencyGraph.setNode(node.name.value); 103 | break; 104 | } 105 | case Kind.UNION_TYPE_DEFINITION: { 106 | const dependency = node.name.value; 107 | if (!dependencyGraph.hasNode(dependency)) 108 | dependencyGraph.setNode(dependency); 109 | 110 | node.types?.forEach((type) => { 111 | const dependency = type.name.value; 112 | const typ = schema.getType(dependency); 113 | if (typ?.astNode?.kind === undefined || !targetKinds.includes(typ.astNode.kind)) 114 | return; 115 | 116 | dependencyGraph.setEdge(node.name.value, dependency); 117 | }); 118 | break; 119 | } 120 | default: 121 | break; 122 | } 123 | }, 124 | }); 125 | 126 | const sorted = topsort(dependencyGraph); 127 | 128 | // Create a map of definitions for quick access, using the definition's name as the key. 129 | const definitionsMap: Map = new Map(); 130 | 131 | // SCHEMA_DEFINITION does not have a name. 132 | // https://spec.graphql.org/October2021/#sec-Schema 133 | const astDefinitions = ast.definitions.filter(def => def.kind !== Kind.SCHEMA_DEFINITION); 134 | 135 | astDefinitions.forEach((definition) => { 136 | if (hasNameField(definition) && definition.name) 137 | definitionsMap.set(definition.name.value, definition); 138 | }); 139 | 140 | // Two arrays to store sorted and unsorted definitions. 141 | const sortedDefinitions: DefinitionNode[] = []; 142 | const notSortedDefinitions: DefinitionNode[] = []; 143 | 144 | // Iterate over sorted type names and retrieve their corresponding definitions. 145 | sorted.forEach((sortedType) => { 146 | const definition = definitionsMap.get(sortedType); 147 | if (definition) { 148 | sortedDefinitions.push(definition); 149 | definitionsMap.delete(sortedType); 150 | } 151 | }); 152 | 153 | // Definitions that are left in the map were not included in sorted list 154 | // Add them to notSortedDefinitions. 155 | definitionsMap.forEach(definition => notSortedDefinitions.push(definition)); 156 | 157 | const newDefinitions = [...sortedDefinitions, ...notSortedDefinitions]; 158 | 159 | if (newDefinitions.length !== astDefinitions.length) { 160 | throw new Error( 161 | `Unexpected definition length after sorting: want ${astDefinitions.length} but got ${newDefinitions.length}`, 162 | ); 163 | } 164 | 165 | return { 166 | ...ast, 167 | definitions: newDefinitions as ReadonlyArray, 168 | }; 169 | } 170 | 171 | function hasNameField(node: ASTNode): node is DefinitionNode & { name?: NameNode } { 172 | return 'name' in node; 173 | } 174 | 175 | /** 176 | * [Re-implemented topsort function][topsort-ref] with cycle handling. This version iterates over 177 | * all nodes in the graph to ensure every node is visited, even if the graph contains cycles. 178 | * 179 | * [topsort-ref]: https://github.com/dagrejs/graphlib/blob/8d27cb89029081c72eb89dde652602805bdd0a34/lib/alg/topsort.js 180 | */ 181 | export function topsort(g: Graph): string[] { 182 | const visited = new Set(); 183 | const results: Array = []; 184 | 185 | function visit(node: string) { 186 | if (visited.has(node)) 187 | return; 188 | visited.add(node); 189 | // Recursively visit all predecessors of the node. 190 | g.predecessors(node)?.forEach(visit); 191 | results.push(node); 192 | } 193 | 194 | // Visit every node in the graph (instead of only sinks) 195 | g.nodes().forEach(visit); 196 | 197 | return results.reverse(); 198 | } 199 | 200 | export function isGeneratedByIntrospection(schema: GraphQLSchema): boolean { 201 | return Object.entries(schema.getTypeMap()) 202 | .filter(([name, type]) => !name.startsWith('__') && !isSpecifiedScalarType(type)) 203 | .every(([, type]) => type.astNode === undefined) 204 | } 205 | 206 | // https://spec.graphql.org/October2021/#EscapedCharacter 207 | const escapeMap: { [key: string]: string } = { 208 | '\"': '\\\"', 209 | '\\': '\\\\', 210 | '\/': '\\/', 211 | '\b': '\\b', 212 | '\f': '\\f', 213 | '\n': '\\n', 214 | '\r': '\\r', 215 | '\t': '\\t', 216 | }; 217 | 218 | export function escapeGraphQLCharacters(input: string): string { 219 | // eslint-disable-next-line regexp/no-escape-backspace 220 | return input.replace(/["\\/\f\n\r\t\b]/g, match => escapeMap[match]); 221 | } 222 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { PluginFunction, Types } from '@graphql-codegen/plugin-helpers'; 2 | import type { GraphQLSchema } from 'graphql'; 3 | import type { ValidationSchemaPluginConfig } from './config.js'; 4 | import type { SchemaVisitor } from './types.js'; 5 | import { transformSchemaAST } from '@graphql-codegen/schema-ast'; 6 | import { buildSchema, printSchema, visit } from 'graphql'; 7 | 8 | import { isGeneratedByIntrospection, topologicalSortAST } from './graphql.js'; 9 | import { MyZodSchemaVisitor } from './myzod/index.js'; 10 | import { ValibotSchemaVisitor } from './valibot/index.js'; 11 | import { YupSchemaVisitor } from './yup/index.js'; 12 | import { ZodSchemaVisitor } from './zod/index.js'; 13 | 14 | export const plugin: PluginFunction = ( 15 | schema: GraphQLSchema, 16 | _documents: Types.DocumentFile[], 17 | config: ValidationSchemaPluginConfig, 18 | ): Types.ComplexPluginOutput => { 19 | const { schema: _schema, ast } = _transformSchemaAST(schema, config); 20 | const visitor = schemaVisitor(_schema, config); 21 | 22 | const result = visit(ast, visitor); 23 | 24 | const generated = result.definitions.filter(def => typeof def === 'string'); 25 | 26 | return { 27 | prepend: visitor.buildImports(), 28 | content: [visitor.initialEmit(), ...generated].join('\n'), 29 | }; 30 | }; 31 | 32 | function schemaVisitor(schema: GraphQLSchema, config: ValidationSchemaPluginConfig): SchemaVisitor { 33 | if (config?.schema === 'zod') 34 | return new ZodSchemaVisitor(schema, config); 35 | else if (config?.schema === 'myzod') 36 | return new MyZodSchemaVisitor(schema, config); 37 | else if (config?.schema === 'valibot') 38 | return new ValibotSchemaVisitor(schema, config); 39 | 40 | return new YupSchemaVisitor(schema, config); 41 | } 42 | 43 | function _transformSchemaAST(schema: GraphQLSchema, config: ValidationSchemaPluginConfig) { 44 | const { schema: _schema, ast } = transformSchemaAST(schema, config); 45 | 46 | // See: https://github.com/Code-Hex/graphql-codegen-typescript-validation-schema/issues/394 47 | const __schema = isGeneratedByIntrospection(_schema) ? buildSchema(printSchema(_schema)) : _schema; 48 | 49 | // This affects the performance of code generation, so it is 50 | // enabled only when this option is selected. 51 | if (config.validationSchemaExportType === 'const') { 52 | return { 53 | schema: __schema, 54 | ast: topologicalSortAST(__schema, ast), 55 | }; 56 | } 57 | return { 58 | schema: __schema, 59 | ast, 60 | }; 61 | } 62 | 63 | export type { ValidationSchemaPluginConfig } 64 | -------------------------------------------------------------------------------- /src/myzod/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | EnumTypeDefinitionNode, 3 | FieldDefinitionNode, 4 | GraphQLSchema, 5 | InputObjectTypeDefinitionNode, 6 | InputValueDefinitionNode, 7 | InterfaceTypeDefinitionNode, 8 | NameNode, 9 | ObjectTypeDefinitionNode, 10 | TypeNode, 11 | UnionTypeDefinitionNode, 12 | } from 'graphql'; 13 | 14 | import type { ValidationSchemaPluginConfig } from '../config.js'; 15 | import type { Visitor } from '../visitor.js'; 16 | import { resolveExternalModuleAndFn } from '@graphql-codegen/plugin-helpers'; 17 | import { convertNameParts, DeclarationBlock, indent } from '@graphql-codegen/visitor-plugin-common'; 18 | import { 19 | Kind, 20 | } from 'graphql'; 21 | import { buildApi, formatDirectiveConfig } from '../directive.js'; 22 | import { 23 | escapeGraphQLCharacters, 24 | InterfaceTypeDefinitionBuilder, 25 | isInput, 26 | isListType, 27 | isNamedType, 28 | isNonNullType, 29 | ObjectTypeDefinitionBuilder, 30 | } from '../graphql.js'; 31 | import { BaseSchemaVisitor } from '../schema_visitor.js'; 32 | 33 | const anySchema = `definedNonNullAnySchema`; 34 | 35 | export class MyZodSchemaVisitor extends BaseSchemaVisitor { 36 | constructor(schema: GraphQLSchema, config: ValidationSchemaPluginConfig) { 37 | super(schema, config); 38 | } 39 | 40 | importValidationSchema(): string { 41 | return `import * as myzod from 'myzod'`; 42 | } 43 | 44 | initialEmit(): string { 45 | return ( 46 | `\n${ 47 | [ 48 | new DeclarationBlock({}).export().asKind('const').withName(`${anySchema}`).withContent(`myzod.object({})`).string, 49 | ...this.enumDeclarations, 50 | ].join('\n')}` 51 | ); 52 | } 53 | 54 | get InputObjectTypeDefinition() { 55 | return { 56 | leave: (node: InputObjectTypeDefinitionNode) => { 57 | const visitor = this.createVisitor('input'); 58 | const name = visitor.convertName(node.name.value); 59 | this.importTypes.push(name); 60 | return this.buildInputFields(node.fields ?? [], visitor, name); 61 | }, 62 | }; 63 | } 64 | 65 | get InterfaceTypeDefinition() { 66 | return { 67 | leave: InterfaceTypeDefinitionBuilder(this.config.withObjectType, (node: InterfaceTypeDefinitionNode) => { 68 | const visitor = this.createVisitor('output'); 69 | const name = visitor.convertName(node.name.value); 70 | const typeName = visitor.prefixTypeNamespace(name); 71 | this.importTypes.push(name); 72 | 73 | // Building schema for field arguments. 74 | const argumentBlocks = this.buildTypeDefinitionArguments(node, visitor); 75 | const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : ''; 76 | 77 | // Building schema for fields. 78 | const shape = node.fields?.map(field => generateFieldMyZodSchema(this.config, visitor, field, 2)).join(',\n'); 79 | 80 | switch (this.config.validationSchemaExportType) { 81 | case 'const': 82 | return ( 83 | new DeclarationBlock({}) 84 | .export() 85 | .asKind('const') 86 | .withName(`${name}Schema: myzod.Type<${typeName}>`) 87 | .withContent([`myzod.object({`, shape, '})'].join('\n')) 88 | .string + appendArguments 89 | ); 90 | 91 | case 'function': 92 | default: 93 | return ( 94 | new DeclarationBlock({}) 95 | .export() 96 | .asKind('function') 97 | .withName(`${name}Schema(): myzod.Type<${typeName}>`) 98 | .withBlock([indent(`return myzod.object({`), shape, indent('})')].join('\n')) 99 | .string + appendArguments 100 | ); 101 | } 102 | }), 103 | }; 104 | } 105 | 106 | get ObjectTypeDefinition() { 107 | return { 108 | leave: ObjectTypeDefinitionBuilder(this.config.withObjectType, (node: ObjectTypeDefinitionNode) => { 109 | const visitor = this.createVisitor('output'); 110 | const name = visitor.convertName(node.name.value); 111 | const typeName = visitor.prefixTypeNamespace(name); 112 | this.importTypes.push(name); 113 | 114 | // Building schema for field arguments. 115 | const argumentBlocks = this.buildTypeDefinitionArguments(node, visitor); 116 | const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : ''; 117 | 118 | // Building schema for fields. 119 | const shape = node.fields?.map(field => generateFieldMyZodSchema(this.config, visitor, field, 2)).join(',\n'); 120 | 121 | switch (this.config.validationSchemaExportType) { 122 | case 'const': 123 | return ( 124 | new DeclarationBlock({}) 125 | .export() 126 | .asKind('const') 127 | .withName(`${name}Schema: myzod.Type<${typeName}>`) 128 | .withContent( 129 | [ 130 | `myzod.object({`, 131 | indent(`__typename: myzod.literal('${node.name.value}').optional(),`, 2), 132 | shape, 133 | '})', 134 | ].join('\n'), 135 | ) 136 | .string + appendArguments 137 | ); 138 | 139 | case 'function': 140 | default: 141 | return ( 142 | new DeclarationBlock({}) 143 | .export() 144 | .asKind('function') 145 | .withName(`${name}Schema(): myzod.Type<${typeName}>`) 146 | .withBlock( 147 | [ 148 | indent(`return myzod.object({`), 149 | indent(`__typename: myzod.literal('${node.name.value}').optional(),`, 2), 150 | shape, 151 | indent('})'), 152 | ].join('\n'), 153 | ) 154 | .string + appendArguments 155 | ); 156 | } 157 | }), 158 | }; 159 | } 160 | 161 | get EnumTypeDefinition() { 162 | return { 163 | leave: (node: EnumTypeDefinitionNode) => { 164 | const visitor = this.createVisitor('both'); 165 | const enumname = visitor.convertName(node.name.value); 166 | const enumTypeName = visitor.prefixTypeNamespace(enumname); 167 | this.importTypes.push(enumname); 168 | // z.enum are basically myzod.literals 169 | // hoist enum declarations 170 | this.enumDeclarations.push( 171 | this.config.enumsAsTypes 172 | ? new DeclarationBlock({}) 173 | .export() 174 | .asKind('type') 175 | .withName(`${enumname}Schema`) 176 | .withContent( 177 | `myzod.literals(${node.values?.map(enumOption => `'${enumOption.name.value}'`).join(', ')})`, 178 | ) 179 | .string 180 | : new DeclarationBlock({}) 181 | .export() 182 | .asKind('const') 183 | .withName(`${enumname}Schema`) 184 | .withContent(`myzod.enum(${enumTypeName})`) 185 | .string, 186 | ); 187 | }, 188 | }; 189 | } 190 | 191 | get UnionTypeDefinition() { 192 | return { 193 | leave: (node: UnionTypeDefinitionNode) => { 194 | if (!node.types || !this.config.withObjectType) 195 | return; 196 | 197 | const visitor = this.createVisitor('output'); 198 | 199 | const unionName = visitor.convertName(node.name.value); 200 | const unionElements = node.types?.map((t) => { 201 | const element = visitor.convertName(t.name.value); 202 | const typ = visitor.getType(t.name.value); 203 | if (typ?.astNode?.kind === 'EnumTypeDefinition') 204 | return `${element}Schema`; 205 | 206 | switch (this.config.validationSchemaExportType) { 207 | case 'const': 208 | return `${element}Schema`; 209 | case 'function': 210 | default: 211 | return `${element}Schema()`; 212 | } 213 | }).join(', '); 214 | const unionElementsCount = node.types?.length ?? 0; 215 | 216 | const union = unionElementsCount > 1 ? `myzod.union([${unionElements}])` : unionElements; 217 | 218 | switch (this.config.validationSchemaExportType) { 219 | case 'const': 220 | return new DeclarationBlock({}).export().asKind('const').withName(`${unionName}Schema`).withContent(union).string; 221 | case 'function': 222 | default: 223 | return new DeclarationBlock({}) 224 | .export() 225 | .asKind('function') 226 | .withName(`${unionName}Schema()`) 227 | .withBlock(indent(`return ${union}`)) 228 | .string; 229 | } 230 | }, 231 | }; 232 | } 233 | 234 | protected buildInputFields( 235 | fields: readonly (FieldDefinitionNode | InputValueDefinitionNode)[], 236 | visitor: Visitor, 237 | name: string, 238 | ) { 239 | const typeName = visitor.prefixTypeNamespace(name); 240 | const shape = fields.map(field => generateFieldMyZodSchema(this.config, visitor, field, 2)).join(',\n'); 241 | 242 | switch (this.config.validationSchemaExportType) { 243 | case 'const': 244 | return new DeclarationBlock({}) 245 | .export() 246 | .asKind('const') 247 | .withName(`${name}Schema: myzod.Type<${typeName}>`) 248 | .withContent(['myzod.object({', shape, '})'].join('\n')) 249 | .string; 250 | 251 | case 'function': 252 | default: 253 | return new DeclarationBlock({}) 254 | .export() 255 | .asKind('function') 256 | .withName(`${name}Schema(): myzod.Type<${typeName}>`) 257 | .withBlock([indent(`return myzod.object({`), shape, indent('})')].join('\n')) 258 | .string; 259 | } 260 | } 261 | } 262 | 263 | function generateFieldMyZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, indentCount: number): string { 264 | const gen = generateFieldTypeMyZodSchema(config, visitor, field, field.type); 265 | return indent(`${field.name.value}: ${maybeLazy(field.type, gen)}`, indentCount); 266 | } 267 | 268 | function generateFieldTypeMyZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, type: TypeNode, parentType?: TypeNode): string { 269 | if (isListType(type)) { 270 | const gen = generateFieldTypeMyZodSchema(config, visitor, field, type.type, type); 271 | if (!isNonNullType(parentType)) { 272 | const arrayGen = `myzod.array(${maybeLazy(type.type, gen)})`; 273 | const maybeLazyGen = applyDirectives(config, field, arrayGen); 274 | return `${maybeLazyGen}.optional().nullable()`; 275 | } 276 | return `myzod.array(${maybeLazy(type.type, gen)})`; 277 | } 278 | if (isNonNullType(type)) { 279 | const gen = generateFieldTypeMyZodSchema(config, visitor, field, type.type, type); 280 | return maybeLazy(type.type, gen); 281 | } 282 | if (isNamedType(type)) { 283 | const gen = generateNameNodeMyZodSchema(config, visitor, type.name); 284 | if (isListType(parentType)) 285 | return `${gen}.nullable()`; 286 | 287 | let appliedDirectivesGen = applyDirectives(config, field, gen); 288 | 289 | if (field.kind === Kind.INPUT_VALUE_DEFINITION) { 290 | const { defaultValue } = field; 291 | 292 | if (defaultValue?.kind === Kind.INT || defaultValue?.kind === Kind.FLOAT || defaultValue?.kind === Kind.BOOLEAN) 293 | appliedDirectivesGen = `${appliedDirectivesGen}.default(${defaultValue.value})`; 294 | 295 | if (defaultValue?.kind === Kind.STRING || defaultValue?.kind === Kind.ENUM) { 296 | if (config.useEnumTypeAsDefaultValue && defaultValue?.kind !== Kind.STRING) { 297 | let value = convertNameParts(defaultValue.value, resolveExternalModuleAndFn('change-case-all#pascalCase'), config?.namingConvention?.transformUnderscore); 298 | 299 | if (config.namingConvention?.enumValues) 300 | value = convertNameParts(defaultValue.value, resolveExternalModuleAndFn(config.namingConvention?.enumValues), config?.namingConvention?.transformUnderscore); 301 | 302 | appliedDirectivesGen = `${appliedDirectivesGen}.default(${visitor.convertName(type.name.value)}.${value})`; 303 | } 304 | else { 305 | appliedDirectivesGen = `${appliedDirectivesGen}.default("${escapeGraphQLCharacters(defaultValue.value)}")`; 306 | } 307 | } 308 | } 309 | 310 | if (isNonNullType(parentType)) { 311 | if (visitor.shouldEmitAsNotAllowEmptyString(type.name.value)) 312 | return `${gen}.min(1)`; 313 | 314 | return appliedDirectivesGen; 315 | } 316 | if (isListType(parentType)) 317 | return `${appliedDirectivesGen}.nullable()`; 318 | 319 | return `${appliedDirectivesGen}.optional().nullable()`; 320 | } 321 | console.warn('unhandled type:', type); 322 | return ''; 323 | } 324 | 325 | function applyDirectives(config: ValidationSchemaPluginConfig, field: InputValueDefinitionNode | FieldDefinitionNode, gen: string): string { 326 | if (config.directives && field.directives) { 327 | const formatted = formatDirectiveConfig(config.directives); 328 | return gen + buildApi(formatted, field.directives); 329 | } 330 | return gen; 331 | } 332 | 333 | function generateNameNodeMyZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, node: NameNode): string { 334 | const converter = visitor.getNameNodeConverter(node); 335 | 336 | switch (converter?.targetKind) { 337 | case 'InterfaceTypeDefinition': 338 | case 'InputObjectTypeDefinition': 339 | case 'ObjectTypeDefinition': 340 | case 'UnionTypeDefinition': 341 | // using switch-case rather than if-else to allow for future expansion 342 | switch (config.validationSchemaExportType) { 343 | case 'const': 344 | return `${converter.convertName()}Schema`; 345 | case 'function': 346 | default: 347 | return `${converter.convertName()}Schema()`; 348 | } 349 | case 'EnumTypeDefinition': 350 | return `${converter.convertName()}Schema`; 351 | case 'ScalarTypeDefinition': 352 | return myzod4Scalar(config, visitor, node.value); 353 | default: 354 | if (converter?.targetKind) 355 | console.warn('Unknown target kind', converter.targetKind); 356 | 357 | return myzod4Scalar(config, visitor, node.value); 358 | } 359 | } 360 | 361 | function maybeLazy(type: TypeNode, schema: string): string { 362 | if (isNamedType(type) && isInput(type.name.value)) 363 | return `myzod.lazy(() => ${schema})`; 364 | 365 | return schema; 366 | } 367 | 368 | function myzod4Scalar(config: ValidationSchemaPluginConfig, visitor: Visitor, scalarName: string): string { 369 | if (config.scalarSchemas?.[scalarName]) 370 | return config.scalarSchemas[scalarName]; 371 | 372 | const tsType = visitor.getScalarType(scalarName); 373 | switch (tsType) { 374 | case 'string': 375 | return `myzod.string()`; 376 | case 'number': 377 | return `myzod.number()`; 378 | case 'boolean': 379 | return `myzod.boolean()`; 380 | } 381 | 382 | if (config.defaultScalarTypeSchema) { 383 | return config.defaultScalarTypeSchema; 384 | } 385 | 386 | console.warn('unhandled name:', scalarName); 387 | return anySchema; 388 | } 389 | -------------------------------------------------------------------------------- /src/regexp.ts: -------------------------------------------------------------------------------- 1 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#advanced_searching_with_flags 2 | export const isConvertableRegexp = (maybeRegexp: string): boolean => /^\/.*\/[dgimsuy]*$/.test(maybeRegexp); 3 | -------------------------------------------------------------------------------- /src/schema_visitor.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | FieldDefinitionNode, 3 | GraphQLSchema, 4 | InputValueDefinitionNode, 5 | InterfaceTypeDefinitionNode, 6 | ObjectTypeDefinitionNode, 7 | } from 'graphql'; 8 | 9 | import type { ValidationSchemaPluginConfig } from './config.js'; 10 | import type { SchemaVisitor } from './types.js'; 11 | import { Visitor } from './visitor.js'; 12 | 13 | export abstract class BaseSchemaVisitor implements SchemaVisitor { 14 | protected importTypes: string[] = []; 15 | protected enumDeclarations: string[] = []; 16 | 17 | constructor( 18 | protected schema: GraphQLSchema, 19 | protected config: ValidationSchemaPluginConfig, 20 | ) {} 21 | 22 | abstract importValidationSchema(): string; 23 | 24 | buildImports(): string[] { 25 | if (this.config.importFrom && this.importTypes.length > 0) { 26 | const namedImportPrefix = this.config.useTypeImports ? 'type ' : ''; 27 | 28 | const importCore = this.config.schemaNamespacedImportName 29 | ? `* as ${this.config.schemaNamespacedImportName}` 30 | : `${namedImportPrefix}{ ${this.importTypes.join(', ')} }`; 31 | 32 | return [ 33 | this.importValidationSchema(), 34 | `import ${importCore} from '${this.config.importFrom}'`, 35 | ]; 36 | } 37 | return [this.importValidationSchema()]; 38 | } 39 | 40 | abstract initialEmit(): string; 41 | 42 | createVisitor(scalarDirection: 'input' | 'output' | 'both'): Visitor { 43 | return new Visitor(scalarDirection, this.schema, this.config); 44 | } 45 | 46 | protected abstract buildInputFields( 47 | fields: readonly (FieldDefinitionNode | InputValueDefinitionNode)[], 48 | visitor: Visitor, 49 | name: string 50 | ): string; 51 | 52 | protected buildTypeDefinitionArguments( 53 | node: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, 54 | visitor: Visitor, 55 | ) { 56 | return visitor.buildArgumentsSchemaBlock(node, (typeName, field) => { 57 | this.importTypes.push(typeName); 58 | return this.buildInputFields(field.arguments ?? [], visitor, typeName); 59 | }); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { ASTNode, ASTVisitFn } from 'graphql'; 2 | 3 | export type NewVisitor = Partial<{ 4 | readonly [NodeT in ASTNode as NodeT['kind']]?: { 5 | leave?: ASTVisitFn 6 | }; 7 | }>; 8 | 9 | export interface SchemaVisitor extends NewVisitor { 10 | buildImports: () => string[] 11 | initialEmit: () => string 12 | } 13 | -------------------------------------------------------------------------------- /src/valibot/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | EnumTypeDefinitionNode, 3 | FieldDefinitionNode, 4 | GraphQLSchema, 5 | InputObjectTypeDefinitionNode, 6 | InputValueDefinitionNode, 7 | InterfaceTypeDefinitionNode, 8 | NameNode, 9 | ObjectTypeDefinitionNode, 10 | TypeNode, 11 | UnionTypeDefinitionNode, 12 | } from 'graphql'; 13 | import type { ValidationSchemaPluginConfig } from '../config.js'; 14 | import type { Visitor } from '../visitor.js'; 15 | 16 | import { DeclarationBlock, indent } from '@graphql-codegen/visitor-plugin-common'; 17 | import { buildApiForValibot, formatDirectiveConfig } from '../directive.js'; 18 | import { 19 | InterfaceTypeDefinitionBuilder, 20 | isInput, 21 | isListType, 22 | isNamedType, 23 | isNonNullType, 24 | ObjectTypeDefinitionBuilder, 25 | } from '../graphql.js'; 26 | import { BaseSchemaVisitor } from '../schema_visitor.js'; 27 | 28 | export class ValibotSchemaVisitor extends BaseSchemaVisitor { 29 | constructor(schema: GraphQLSchema, config: ValidationSchemaPluginConfig) { 30 | super(schema, config); 31 | } 32 | 33 | importValidationSchema(): string { 34 | return `import * as v from 'valibot'`; 35 | } 36 | 37 | initialEmit(): string { 38 | return ( 39 | `\n${[ 40 | ...this.enumDeclarations, 41 | ].join('\n')}` 42 | ); 43 | } 44 | 45 | get InputObjectTypeDefinition() { 46 | return { 47 | leave: (node: InputObjectTypeDefinitionNode) => { 48 | const visitor = this.createVisitor('input'); 49 | const name = visitor.convertName(node.name.value); 50 | this.importTypes.push(name); 51 | return this.buildInputFields(node.fields ?? [], visitor, name); 52 | }, 53 | }; 54 | } 55 | 56 | get InterfaceTypeDefinition() { 57 | return { 58 | leave: InterfaceTypeDefinitionBuilder(this.config.withObjectType, (node: InterfaceTypeDefinitionNode) => { 59 | const visitor = this.createVisitor('output'); 60 | const name = visitor.convertName(node.name.value); 61 | const typeName = visitor.prefixTypeNamespace(name); 62 | this.importTypes.push(name); 63 | 64 | // Building schema for field arguments. 65 | const argumentBlocks = this.buildTypeDefinitionArguments(node, visitor); 66 | const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : ''; 67 | 68 | // Building schema for fields. 69 | const shape = node.fields?.map(field => generateFieldValibotSchema(this.config, visitor, field, 2)).join(',\n'); 70 | 71 | switch (this.config.validationSchemaExportType) { 72 | default: 73 | return ( 74 | new DeclarationBlock({}) 75 | .export() 76 | .asKind('function') 77 | .withName(`${name}Schema(): v.GenericSchema<${typeName}>`) 78 | .withBlock([indent(`return v.object({`), shape, indent('})')].join('\n')) 79 | .string + appendArguments 80 | ); 81 | } 82 | }), 83 | }; 84 | } 85 | 86 | get ObjectTypeDefinition() { 87 | return { 88 | leave: ObjectTypeDefinitionBuilder(this.config.withObjectType, (node: ObjectTypeDefinitionNode) => { 89 | const visitor = this.createVisitor('output'); 90 | const name = visitor.convertName(node.name.value); 91 | const typeName = visitor.prefixTypeNamespace(name); 92 | this.importTypes.push(name); 93 | 94 | // Building schema for field arguments. 95 | const argumentBlocks = this.buildTypeDefinitionArguments(node, visitor); 96 | const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : ''; 97 | 98 | // Building schema for fields. 99 | const shape = node.fields?.map(field => generateFieldValibotSchema(this.config, visitor, field, 2)).join(',\n'); 100 | 101 | switch (this.config.validationSchemaExportType) { 102 | default: 103 | return ( 104 | new DeclarationBlock({}) 105 | .export() 106 | .asKind('function') 107 | .withName(`${name}Schema(): v.GenericSchema<${typeName}>`) 108 | .withBlock( 109 | [ 110 | indent(`return v.object({`), 111 | indent(`__typename: v.optional(v.literal('${node.name.value}')),`, 2), 112 | shape, 113 | indent('})'), 114 | ].join('\n'), 115 | ) 116 | .string + appendArguments 117 | ); 118 | } 119 | }), 120 | }; 121 | } 122 | 123 | get EnumTypeDefinition() { 124 | return { 125 | leave: (node: EnumTypeDefinitionNode) => { 126 | const visitor = this.createVisitor('both'); 127 | const enumname = visitor.convertName(node.name.value); 128 | const enumTypeName = visitor.prefixTypeNamespace(enumname); 129 | this.importTypes.push(enumname); 130 | 131 | // hoist enum declarations 132 | this.enumDeclarations.push( 133 | this.config.enumsAsTypes 134 | ? new DeclarationBlock({}) 135 | .export() 136 | .asKind('const') 137 | .withName(`${enumname}Schema`) 138 | .withContent(`v.picklist([${node.values?.map(enumOption => `'${enumOption.name.value}'`).join(', ')}])`) 139 | .string 140 | : new DeclarationBlock({}) 141 | .export() 142 | .asKind('const') 143 | .withName(`${enumname}Schema`) 144 | .withContent(`v.enum_(${enumTypeName})`) 145 | .string, 146 | ); 147 | }, 148 | }; 149 | } 150 | 151 | get UnionTypeDefinition() { 152 | return { 153 | leave: (node: UnionTypeDefinitionNode) => { 154 | if (!node.types || !this.config.withObjectType) 155 | return; 156 | const visitor = this.createVisitor('output'); 157 | const unionName = visitor.convertName(node.name.value); 158 | const unionElements = node.types.map((t) => { 159 | const element = visitor.convertName(t.name.value); 160 | const typ = visitor.getType(t.name.value); 161 | if (typ?.astNode?.kind === 'EnumTypeDefinition') 162 | return `${element}Schema`; 163 | 164 | switch (this.config.validationSchemaExportType) { 165 | default: 166 | return `${element}Schema()`; 167 | } 168 | }).join(', '); 169 | const unionElementsCount = node.types.length ?? 0; 170 | 171 | const union = unionElementsCount > 1 ? `v.union([${unionElements}])` : unionElements; 172 | 173 | switch (this.config.validationSchemaExportType) { 174 | default: 175 | return new DeclarationBlock({}) 176 | .export() 177 | .asKind('function') 178 | .withName(`${unionName}Schema()`) 179 | .withBlock(indent(`return ${union}`)) 180 | .string; 181 | } 182 | }, 183 | }; 184 | } 185 | 186 | protected buildInputFields( 187 | fields: readonly (FieldDefinitionNode | InputValueDefinitionNode)[], 188 | visitor: Visitor, 189 | name: string, 190 | ) { 191 | const typeName = visitor.prefixTypeNamespace(name); 192 | const shape = fields.map(field => generateFieldValibotSchema(this.config, visitor, field, 2)).join(',\n'); 193 | 194 | switch (this.config.validationSchemaExportType) { 195 | default: 196 | return new DeclarationBlock({}) 197 | .export() 198 | .asKind('function') 199 | .withName(`${name}Schema(): v.GenericSchema<${typeName}>`) 200 | .withBlock([indent(`return v.object({`), shape, indent('})')].join('\n')) 201 | .string; 202 | } 203 | } 204 | } 205 | 206 | function generateFieldValibotSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, indentCount: number): string { 207 | const gen = generateFieldTypeValibotSchema(config, visitor, field, field.type); 208 | return indent(`${field.name.value}: ${maybeLazy(field.type, gen)}`, indentCount); 209 | } 210 | 211 | function generateFieldTypeValibotSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, type: TypeNode, parentType?: TypeNode): string { 212 | if (isListType(type)) { 213 | const gen = generateFieldTypeValibotSchema(config, visitor, field, type.type, type); 214 | const arrayGen = `v.array(${maybeLazy(type.type, gen)})`; 215 | if (!isNonNullType(parentType)) 216 | return `v.nullish(${arrayGen})`; 217 | 218 | return arrayGen; 219 | } 220 | if (isNonNullType(type)) { 221 | const gen = generateFieldTypeValibotSchema(config, visitor, field, type.type, type); 222 | return maybeLazy(type.type, gen); 223 | } 224 | if (isNamedType(type)) { 225 | const gen = generateNameNodeValibotSchema(config, visitor, type.name); 226 | if (isListType(parentType)) 227 | return `v.nullable(${gen})`; 228 | 229 | const actions = actionsFromDirectives(config, field); 230 | 231 | if (isNonNullType(parentType)) { 232 | if (visitor.shouldEmitAsNotAllowEmptyString(type.name.value)) { 233 | actions.push('v.minLength(1)'); 234 | } 235 | 236 | return pipeSchemaAndActions(gen, actions); 237 | } 238 | 239 | return `v.nullish(${pipeSchemaAndActions(gen, actions)})`; 240 | } 241 | console.warn('unhandled type:', type); 242 | return ''; 243 | } 244 | 245 | function actionsFromDirectives(config: ValidationSchemaPluginConfig, field: InputValueDefinitionNode | FieldDefinitionNode): string[] { 246 | if (config.directives && field.directives) { 247 | const formatted = formatDirectiveConfig(config.directives); 248 | return buildApiForValibot(formatted, field.directives); 249 | } 250 | 251 | return []; 252 | } 253 | 254 | function pipeSchemaAndActions(schema: string, actions: string[]): string { 255 | if (actions.length === 0) 256 | return schema; 257 | 258 | return `v.pipe(${schema}, ${actions.join(', ')})`; 259 | } 260 | 261 | function generateNameNodeValibotSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, node: NameNode): string { 262 | const converter = visitor.getNameNodeConverter(node); 263 | 264 | switch (converter?.targetKind) { 265 | case 'InterfaceTypeDefinition': 266 | case 'InputObjectTypeDefinition': 267 | case 'ObjectTypeDefinition': 268 | case 'UnionTypeDefinition': 269 | // using switch-case rather than if-else to allow for future expansion 270 | switch (config.validationSchemaExportType) { 271 | default: 272 | return `${converter.convertName()}Schema()`; 273 | } 274 | case 'EnumTypeDefinition': 275 | return `${converter.convertName()}Schema`; 276 | case 'ScalarTypeDefinition': 277 | return valibot4Scalar(config, visitor, node.value); 278 | default: 279 | if (converter?.targetKind) 280 | console.warn('Unknown targetKind', converter?.targetKind); 281 | 282 | return valibot4Scalar(config, visitor, node.value); 283 | } 284 | } 285 | 286 | function maybeLazy(type: TypeNode, schema: string): string { 287 | if (isNamedType(type) && isInput(type.name.value)) 288 | return `v.lazy(() => ${schema})`; 289 | 290 | return schema; 291 | } 292 | 293 | function valibot4Scalar(config: ValidationSchemaPluginConfig, visitor: Visitor, scalarName: string): string { 294 | if (config.scalarSchemas?.[scalarName]) 295 | return config.scalarSchemas[scalarName]; 296 | 297 | const tsType = visitor.getScalarType(scalarName); 298 | switch (tsType) { 299 | case 'string': 300 | return `v.string()`; 301 | case 'number': 302 | return `v.number()`; 303 | case 'boolean': 304 | return `v.boolean()`; 305 | } 306 | 307 | if (config.defaultScalarTypeSchema) { 308 | return config.defaultScalarTypeSchema; 309 | } 310 | 311 | console.warn('unhandled scalar name:', scalarName); 312 | return 'v.any()'; 313 | } 314 | -------------------------------------------------------------------------------- /src/visitor.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | FieldDefinitionNode, 3 | GraphQLSchema, 4 | InterfaceTypeDefinitionNode, 5 | NameNode, 6 | ObjectTypeDefinitionNode, 7 | } from 'graphql'; 8 | import type { ValidationSchemaPluginConfig } from './config.js'; 9 | 10 | import { TsVisitor } from '@graphql-codegen/typescript'; 11 | import { 12 | specifiedScalarTypes, 13 | } from 'graphql'; 14 | 15 | export class Visitor extends TsVisitor { 16 | constructor( 17 | private scalarDirection: 'input' | 'output' | 'both', 18 | private schema: GraphQLSchema, 19 | private pluginConfig: ValidationSchemaPluginConfig, 20 | ) { 21 | super(schema, pluginConfig); 22 | } 23 | 24 | public prefixTypeNamespace(type: string): string { 25 | if (this.pluginConfig.importFrom && this.pluginConfig.schemaNamespacedImportName) { 26 | return `${this.pluginConfig.schemaNamespacedImportName}.${type}`; 27 | } 28 | 29 | return type; 30 | } 31 | 32 | private isSpecifiedScalarName(scalarName: string) { 33 | return specifiedScalarTypes.some(({ name }) => name === scalarName); 34 | } 35 | 36 | public getType(name: string) { 37 | return this.schema.getType(name); 38 | } 39 | 40 | public getNameNodeConverter(node: NameNode) { 41 | const typ = this.schema.getType(node.value); 42 | const astNode = typ?.astNode; 43 | if (astNode === undefined || astNode === null) 44 | return undefined; 45 | 46 | return { 47 | targetKind: astNode.kind, 48 | convertName: () => this.convertName(astNode.name.value), 49 | }; 50 | } 51 | 52 | public getScalarType(scalarName: string): string | null { 53 | if (this.scalarDirection === 'both') 54 | return null; 55 | 56 | const scalar = this.scalars[scalarName]; 57 | if (!scalar) 58 | throw new Error(`Unknown scalar ${scalarName}`); 59 | 60 | return scalar[this.scalarDirection]; 61 | } 62 | 63 | public shouldEmitAsNotAllowEmptyString(name: string): boolean { 64 | if (this.pluginConfig.notAllowEmptyString !== true) 65 | return false; 66 | 67 | const typ = this.getType(name); 68 | if (typ?.astNode?.kind !== 'ScalarTypeDefinition' && !this.isSpecifiedScalarName(name)) 69 | return false; 70 | 71 | const tsType = this.getScalarType(name); 72 | return tsType === 'string'; 73 | } 74 | 75 | public buildArgumentsSchemaBlock( 76 | node: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, 77 | callback: (typeName: string, field: FieldDefinitionNode) => string, 78 | ) { 79 | const fieldsWithArguments = node.fields?.filter(field => field.arguments && field.arguments.length > 0) ?? []; 80 | if (fieldsWithArguments.length === 0) 81 | return undefined; 82 | 83 | return fieldsWithArguments 84 | .map((field) => { 85 | const name 86 | = `${this.convertName(node.name.value) 87 | + (this.config.addUnderscoreToArgsType ? '_' : '') 88 | + this.convertName(field, { 89 | useTypesPrefix: false, 90 | useTypesSuffix: false, 91 | }) 92 | }Args`; 93 | 94 | return callback(name, field); 95 | }) 96 | .join('\n'); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/yup/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | EnumTypeDefinitionNode, 3 | FieldDefinitionNode, 4 | GraphQLSchema, 5 | InputObjectTypeDefinitionNode, 6 | InputValueDefinitionNode, 7 | InterfaceTypeDefinitionNode, 8 | NameNode, 9 | ObjectTypeDefinitionNode, 10 | TypeNode, 11 | UnionTypeDefinitionNode, 12 | } from 'graphql'; 13 | 14 | import type { ValidationSchemaPluginConfig } from '../config.js'; 15 | import type { Visitor } from '../visitor.js'; 16 | import { resolveExternalModuleAndFn } from '@graphql-codegen/plugin-helpers'; 17 | import { convertNameParts, DeclarationBlock, indent } from '@graphql-codegen/visitor-plugin-common'; 18 | import { 19 | Kind, 20 | } from 'graphql'; 21 | import { buildApi, formatDirectiveConfig } from '../directive.js'; 22 | import { 23 | escapeGraphQLCharacters, 24 | InterfaceTypeDefinitionBuilder, 25 | isInput, 26 | isListType, 27 | isNamedType, 28 | isNonNullType, 29 | ObjectTypeDefinitionBuilder, 30 | } from '../graphql.js'; 31 | import { BaseSchemaVisitor } from '../schema_visitor.js'; 32 | 33 | export class YupSchemaVisitor extends BaseSchemaVisitor { 34 | constructor(schema: GraphQLSchema, config: ValidationSchemaPluginConfig) { 35 | super(schema, config); 36 | } 37 | 38 | importValidationSchema(): string { 39 | return `import * as yup from 'yup'`; 40 | } 41 | 42 | initialEmit(): string { 43 | if (!this.config.withObjectType) 44 | return `\n${this.enumDeclarations.join('\n')}`; 45 | return ( 46 | `\n${ 47 | this.enumDeclarations.join('\n') 48 | }\n${ 49 | new DeclarationBlock({}) 50 | .asKind('function') 51 | .withName('union(...schemas: ReadonlyArray>): yup.MixedSchema') 52 | .withBlock( 53 | [ 54 | indent('return yup.mixed().test({'), 55 | indent('test: (value) => schemas.some((schema) => schema.isValidSync(value))', 2), 56 | indent('}).defined()'), 57 | ].join('\n'), 58 | ) 59 | .string}` 60 | ); 61 | } 62 | 63 | get InputObjectTypeDefinition() { 64 | return { 65 | leave: (node: InputObjectTypeDefinitionNode) => { 66 | const visitor = this.createVisitor('input'); 67 | const name = visitor.convertName(node.name.value); 68 | this.importTypes.push(name); 69 | return this.buildInputFields(node.fields ?? [], visitor, name); 70 | }, 71 | }; 72 | } 73 | 74 | get InterfaceTypeDefinition() { 75 | return { 76 | leave: InterfaceTypeDefinitionBuilder(this.config.withObjectType, (node: InterfaceTypeDefinitionNode) => { 77 | const visitor = this.createVisitor('output'); 78 | const name = visitor.convertName(node.name.value); 79 | const typeName = visitor.prefixTypeNamespace(name); 80 | this.importTypes.push(name); 81 | 82 | // Building schema for field arguments. 83 | const argumentBlocks = this.buildTypeDefinitionArguments(node, visitor); 84 | const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : ''; 85 | 86 | // Building schema for fields. 87 | const shape = node.fields?.map((field) => { 88 | const fieldSchema = generateFieldYupSchema(this.config, visitor, field, 2); 89 | return isNonNullType(field.type) ? fieldSchema : `${fieldSchema}.optional()`; 90 | }).join(',\n'); 91 | 92 | switch (this.config.validationSchemaExportType) { 93 | case 'const': 94 | return ( 95 | new DeclarationBlock({}) 96 | .export() 97 | .asKind('const') 98 | .withName(`${name}Schema: yup.ObjectSchema<${typeName}>`) 99 | .withContent([`yup.object({`, shape, '})'].join('\n')) 100 | .string + appendArguments 101 | ); 102 | 103 | case 'function': 104 | default: 105 | return ( 106 | new DeclarationBlock({}) 107 | .export() 108 | .asKind('function') 109 | .withName(`${name}Schema(): yup.ObjectSchema<${typeName}>`) 110 | .withBlock([indent(`return yup.object({`), shape, indent('})')].join('\n')) 111 | .string + appendArguments 112 | ); 113 | } 114 | }), 115 | }; 116 | } 117 | 118 | get ObjectTypeDefinition() { 119 | return { 120 | leave: ObjectTypeDefinitionBuilder(this.config.withObjectType, (node: ObjectTypeDefinitionNode) => { 121 | const visitor = this.createVisitor('output'); 122 | const name = visitor.convertName(node.name.value); 123 | const typeName = visitor.prefixTypeNamespace(name); 124 | this.importTypes.push(name); 125 | 126 | // Building schema for field arguments. 127 | const argumentBlocks = this.buildTypeDefinitionArguments(node, visitor); 128 | const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : ''; 129 | 130 | // Building schema for fields. 131 | const shape = shapeFields(node.fields, this.config, visitor); 132 | 133 | switch (this.config.validationSchemaExportType) { 134 | case 'const': 135 | return ( 136 | new DeclarationBlock({}) 137 | .export() 138 | .asKind('const') 139 | .withName(`${name}Schema: yup.ObjectSchema<${typeName}>`) 140 | .withContent( 141 | [ 142 | `yup.object({`, 143 | indent(`__typename: yup.string<'${node.name.value}'>().optional(),`, 2), 144 | shape, 145 | '})', 146 | ].join('\n'), 147 | ) 148 | .string + appendArguments 149 | ); 150 | 151 | case 'function': 152 | default: 153 | return ( 154 | new DeclarationBlock({}) 155 | .export() 156 | .asKind('function') 157 | .withName(`${name}Schema(): yup.ObjectSchema<${typeName}>`) 158 | .withBlock( 159 | [ 160 | indent(`return yup.object({`), 161 | indent(`__typename: yup.string<'${node.name.value}'>().optional(),`, 2), 162 | shape, 163 | indent('})'), 164 | ].join('\n'), 165 | ) 166 | .string + appendArguments 167 | ); 168 | } 169 | }), 170 | }; 171 | } 172 | 173 | get EnumTypeDefinition() { 174 | return { 175 | leave: (node: EnumTypeDefinitionNode) => { 176 | const visitor = this.createVisitor('both'); 177 | const enumname = visitor.convertName(node.name.value); 178 | const enumTypeName = visitor.prefixTypeNamespace(enumname); 179 | this.importTypes.push(enumname); 180 | 181 | // hoise enum declarations 182 | if (this.config.enumsAsTypes) { 183 | const enums = node.values?.map(enumOption => `'${enumOption.name.value}'`); 184 | 185 | this.enumDeclarations.push( 186 | new DeclarationBlock({}) 187 | .export() 188 | .asKind('const') 189 | .withName(`${enumname}Schema`) 190 | .withContent(`yup.string().oneOf([${enums?.join(', ')}]).defined()`).string, 191 | ); 192 | } 193 | else { 194 | this.enumDeclarations.push( 195 | new DeclarationBlock({}) 196 | .export() 197 | .asKind('const') 198 | .withName(`${enumname}Schema`) 199 | .withContent(`yup.string<${enumTypeName}>().oneOf(Object.values(${enumTypeName})).defined()`).string, 200 | ); 201 | } 202 | }, 203 | }; 204 | } 205 | 206 | get UnionTypeDefinition() { 207 | return { 208 | leave: (node: UnionTypeDefinitionNode) => { 209 | if (!node.types || !this.config.withObjectType) 210 | return; 211 | const visitor = this.createVisitor('output'); 212 | 213 | const unionName = visitor.convertName(node.name.value); 214 | const unionTypeName = visitor.prefixTypeNamespace(unionName); 215 | this.importTypes.push(unionName); 216 | 217 | const unionElements = node.types?.map((t) => { 218 | const element = visitor.convertName(t.name.value); 219 | const typ = visitor.getType(t.name.value); 220 | if (typ?.astNode?.kind === 'EnumTypeDefinition') 221 | return `${element}Schema`; 222 | 223 | switch (this.config.validationSchemaExportType) { 224 | case 'const': 225 | return `${element}Schema`; 226 | case 'function': 227 | default: 228 | return `${element}Schema()`; 229 | } 230 | }).join(', '); 231 | 232 | switch (this.config.validationSchemaExportType) { 233 | case 'const': 234 | return new DeclarationBlock({}) 235 | .export() 236 | .asKind('const') 237 | .withName(`${unionName}Schema: yup.MixedSchema<${unionTypeName}>`) 238 | .withContent(`union<${unionTypeName}>(${unionElements})`) 239 | .string; 240 | case 'function': 241 | default: 242 | return new DeclarationBlock({}) 243 | .export() 244 | .asKind('function') 245 | .withName(`${unionName}Schema(): yup.MixedSchema<${unionTypeName}>`) 246 | .withBlock(indent(`return union<${unionTypeName}>(${unionElements})`)) 247 | .string; 248 | } 249 | }, 250 | }; 251 | } 252 | 253 | protected buildInputFields( 254 | fields: readonly (FieldDefinitionNode | InputValueDefinitionNode)[], 255 | visitor: Visitor, 256 | name: string, 257 | ) { 258 | const typeName = visitor.prefixTypeNamespace(name); 259 | const shape = shapeFields(fields, this.config, visitor); 260 | 261 | switch (this.config.validationSchemaExportType) { 262 | case 'const': 263 | return new DeclarationBlock({}) 264 | .export() 265 | .asKind('const') 266 | .withName(`${name}Schema: yup.ObjectSchema<${typeName}>`) 267 | .withContent(['yup.object({', shape, '})'].join('\n')) 268 | .string; 269 | 270 | case 'function': 271 | default: 272 | return new DeclarationBlock({}) 273 | .export() 274 | .asKind('function') 275 | .withName(`${name}Schema(): yup.ObjectSchema<${typeName}>`) 276 | .withBlock([indent(`return yup.object({`), shape, indent('})')].join('\n')) 277 | .string; 278 | } 279 | } 280 | } 281 | 282 | function shapeFields(fields: readonly (FieldDefinitionNode | InputValueDefinitionNode)[] | undefined, config: ValidationSchemaPluginConfig, visitor: Visitor) { 283 | return fields 284 | ?.map((field) => { 285 | let fieldSchema = generateFieldYupSchema(config, visitor, field, 2); 286 | 287 | if (field.kind === Kind.INPUT_VALUE_DEFINITION) { 288 | const { defaultValue } = field; 289 | 290 | if ( 291 | defaultValue?.kind === Kind.INT 292 | || defaultValue?.kind === Kind.FLOAT 293 | || defaultValue?.kind === Kind.BOOLEAN 294 | ) { 295 | fieldSchema = `${fieldSchema}.default(${defaultValue.value})`; 296 | } 297 | 298 | if (defaultValue?.kind === Kind.STRING || defaultValue?.kind === Kind.ENUM) { 299 | if (config.useEnumTypeAsDefaultValue && defaultValue?.kind !== Kind.STRING) { 300 | let value = convertNameParts(defaultValue.value, resolveExternalModuleAndFn('change-case-all#pascalCase'), config?.namingConvention?.transformUnderscore); 301 | 302 | if (config.namingConvention?.enumValues) 303 | value = convertNameParts(defaultValue.value, resolveExternalModuleAndFn(config.namingConvention?.enumValues), config?.namingConvention?.transformUnderscore); 304 | const enumName = field.type?.type?.name.value ?? field.name.value 305 | fieldSchema = `${fieldSchema}.default(${visitor.convertName(enumName)}.${value})`; 306 | } 307 | else { 308 | fieldSchema = `${fieldSchema}.default("${escapeGraphQLCharacters(defaultValue.value)}")`; 309 | } 310 | } 311 | } 312 | 313 | if (isNonNullType(field.type)) 314 | return fieldSchema; 315 | 316 | return `${fieldSchema}.optional()`; 317 | }) 318 | .join(',\n'); 319 | } 320 | 321 | function generateFieldYupSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, indentCount: number): string { 322 | let gen = generateFieldTypeYupSchema(config, visitor, field.type); 323 | if (config.directives && field.directives) { 324 | const formatted = formatDirectiveConfig(config.directives); 325 | gen += buildApi(formatted, field.directives); 326 | } 327 | return indent(`${field.name.value}: ${maybeLazy(field.type, gen)}`, indentCount); 328 | } 329 | 330 | function generateFieldTypeYupSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, type: TypeNode, parentType?: TypeNode): string { 331 | if (isListType(type)) { 332 | const gen = generateFieldTypeYupSchema(config, visitor, type.type, type); 333 | if (!isNonNullType(parentType)) 334 | return `yup.array(${maybeLazy(type.type, gen)}).defined().nullable()`; 335 | 336 | return `yup.array(${maybeLazy(type.type, gen)}).defined()`; 337 | } 338 | if (isNonNullType(type)) { 339 | const gen = generateFieldTypeYupSchema(config, visitor, type.type, type); 340 | return maybeLazy(type.type, gen); 341 | } 342 | if (isNamedType(type)) { 343 | const gen = generateNameNodeYupSchema(config, visitor, type.name); 344 | if (isNonNullType(parentType)) { 345 | if (visitor.shouldEmitAsNotAllowEmptyString(type.name.value)) 346 | return `${gen}.required()`; 347 | 348 | return `${gen}.nonNullable()`; 349 | } 350 | const typ = visitor.getType(type.name.value); 351 | if (typ?.astNode?.kind === 'InputObjectTypeDefinition') 352 | return `${gen}`; 353 | 354 | return `${gen}.nullable()`; 355 | } 356 | console.warn('unhandled type:', type); 357 | return ''; 358 | } 359 | 360 | function generateNameNodeYupSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, node: NameNode): string { 361 | const converter = visitor.getNameNodeConverter(node); 362 | 363 | switch (converter?.targetKind) { 364 | case 'InterfaceTypeDefinition': 365 | case 'InputObjectTypeDefinition': 366 | case 'ObjectTypeDefinition': 367 | case 'UnionTypeDefinition': 368 | // using switch-case rather than if-else to allow for future expansion 369 | switch (config.validationSchemaExportType) { 370 | case 'const': 371 | return `${converter.convertName()}Schema`; 372 | case 'function': 373 | default: 374 | return `${converter.convertName()}Schema()`; 375 | } 376 | case 'EnumTypeDefinition': 377 | return `${converter.convertName()}Schema`; 378 | default: 379 | return yup4Scalar(config, visitor, node.value); 380 | } 381 | } 382 | 383 | function maybeLazy(type: TypeNode, schema: string): string { 384 | if (isNamedType(type) && isInput(type.name.value)) { 385 | // https://github.com/jquense/yup/issues/1283#issuecomment-786559444 386 | return `yup.lazy(() => ${schema})`; 387 | } 388 | return schema; 389 | } 390 | 391 | function yup4Scalar(config: ValidationSchemaPluginConfig, visitor: Visitor, scalarName: string): string { 392 | if (config.scalarSchemas?.[scalarName]) 393 | return `${config.scalarSchemas[scalarName]}.defined()`; 394 | 395 | const tsType = visitor.getScalarType(scalarName); 396 | switch (tsType) { 397 | case 'string': 398 | return `yup.string().defined()`; 399 | case 'number': 400 | return `yup.number().defined()`; 401 | case 'boolean': 402 | return `yup.boolean().defined()`; 403 | } 404 | 405 | if (config.defaultScalarTypeSchema) { 406 | return config.defaultScalarTypeSchema 407 | } 408 | 409 | console.warn('unhandled name:', scalarName); 410 | return `yup.mixed()`; 411 | } 412 | -------------------------------------------------------------------------------- /src/zod/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | EnumTypeDefinitionNode, 3 | FieldDefinitionNode, 4 | GraphQLSchema, 5 | InputObjectTypeDefinitionNode, 6 | InputValueDefinitionNode, 7 | InterfaceTypeDefinitionNode, 8 | NameNode, 9 | ObjectTypeDefinitionNode, 10 | TypeNode, 11 | UnionTypeDefinitionNode, 12 | } from 'graphql'; 13 | 14 | import type { ValidationSchemaPluginConfig } from '../config.js'; 15 | import type { Visitor } from '../visitor.js'; 16 | import { resolveExternalModuleAndFn } from '@graphql-codegen/plugin-helpers'; 17 | import { convertNameParts, DeclarationBlock, indent } from '@graphql-codegen/visitor-plugin-common'; 18 | import { 19 | Kind, 20 | } from 'graphql'; 21 | import { buildApi, formatDirectiveConfig } from '../directive.js'; 22 | import { 23 | escapeGraphQLCharacters, 24 | InterfaceTypeDefinitionBuilder, 25 | isInput, 26 | isListType, 27 | isNamedType, 28 | isNonNullType, 29 | ObjectTypeDefinitionBuilder, 30 | } from '../graphql.js'; 31 | import { BaseSchemaVisitor } from '../schema_visitor.js'; 32 | 33 | const anySchema = `definedNonNullAnySchema`; 34 | 35 | export class ZodSchemaVisitor extends BaseSchemaVisitor { 36 | constructor(schema: GraphQLSchema, config: ValidationSchemaPluginConfig) { 37 | super(schema, config); 38 | } 39 | 40 | importValidationSchema(): string { 41 | return `import { z } from 'zod'`; 42 | } 43 | 44 | initialEmit(): string { 45 | return ( 46 | `\n${ 47 | [ 48 | new DeclarationBlock({}) 49 | .asKind('type') 50 | .withName('Properties') 51 | .withContent(['Required<{', ' [K in keyof T]: z.ZodType;', '}>'].join('\n')) 52 | .string, 53 | // Unfortunately, zod doesn’t provide non-null defined any schema. 54 | // This is a temporary hack until it is fixed. 55 | // see: https://github.com/colinhacks/zod/issues/884 56 | new DeclarationBlock({}).asKind('type').withName('definedNonNullAny').withContent('{}').string, 57 | new DeclarationBlock({}) 58 | .export() 59 | .asKind('const') 60 | .withName(`isDefinedNonNullAny`) 61 | .withContent(`(v: any): v is definedNonNullAny => v !== undefined && v !== null`) 62 | .string, 63 | new DeclarationBlock({}) 64 | .export() 65 | .asKind('const') 66 | .withName(`${anySchema}`) 67 | .withContent(`z.any().refine((v) => isDefinedNonNullAny(v))`) 68 | .string, 69 | ...this.enumDeclarations, 70 | ].join('\n')}` 71 | ); 72 | } 73 | 74 | get InputObjectTypeDefinition() { 75 | return { 76 | leave: (node: InputObjectTypeDefinitionNode) => { 77 | const visitor = this.createVisitor('input'); 78 | const name = visitor.convertName(node.name.value); 79 | this.importTypes.push(name); 80 | return this.buildInputFields(node.fields ?? [], visitor, name); 81 | }, 82 | }; 83 | } 84 | 85 | get InterfaceTypeDefinition() { 86 | return { 87 | leave: InterfaceTypeDefinitionBuilder(this.config.withObjectType, (node: InterfaceTypeDefinitionNode) => { 88 | const visitor = this.createVisitor('output'); 89 | const name = visitor.convertName(node.name.value); 90 | const typeName = visitor.prefixTypeNamespace(name); 91 | this.importTypes.push(name); 92 | 93 | // Building schema for field arguments. 94 | const argumentBlocks = this.buildTypeDefinitionArguments(node, visitor); 95 | const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : ''; 96 | 97 | // Building schema for fields. 98 | const shape = node.fields?.map(field => generateFieldZodSchema(this.config, visitor, field, 2)).join(',\n'); 99 | 100 | switch (this.config.validationSchemaExportType) { 101 | case 'const': 102 | return ( 103 | new DeclarationBlock({}) 104 | .export() 105 | .asKind('const') 106 | .withName(`${name}Schema: z.ZodObject>`) 107 | .withContent([`z.object({`, shape, '})'].join('\n')) 108 | .string + appendArguments 109 | ); 110 | 111 | case 'function': 112 | default: 113 | return ( 114 | new DeclarationBlock({}) 115 | .export() 116 | .asKind('function') 117 | .withName(`${name}Schema(): z.ZodObject>`) 118 | .withBlock([indent(`return z.object({`), shape, indent('})')].join('\n')) 119 | .string + appendArguments 120 | ); 121 | } 122 | }), 123 | }; 124 | } 125 | 126 | get ObjectTypeDefinition() { 127 | return { 128 | leave: ObjectTypeDefinitionBuilder(this.config.withObjectType, (node: ObjectTypeDefinitionNode) => { 129 | const visitor = this.createVisitor('output'); 130 | const name = visitor.convertName(node.name.value); 131 | const typeName = visitor.prefixTypeNamespace(name); 132 | this.importTypes.push(name); 133 | 134 | // Building schema for field arguments. 135 | const argumentBlocks = this.buildTypeDefinitionArguments(node, visitor); 136 | const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : ''; 137 | 138 | // Building schema for fields. 139 | const shape = node.fields?.map(field => generateFieldZodSchema(this.config, visitor, field, 2)).join(',\n'); 140 | 141 | switch (this.config.validationSchemaExportType) { 142 | case 'const': 143 | return ( 144 | new DeclarationBlock({}) 145 | .export() 146 | .asKind('const') 147 | .withName(`${name}Schema: z.ZodObject>`) 148 | .withContent( 149 | [ 150 | `z.object({`, 151 | indent(`__typename: z.literal('${node.name.value}').optional(),`, 2), 152 | shape, 153 | '})', 154 | ].join('\n'), 155 | ) 156 | .string + appendArguments 157 | ); 158 | 159 | case 'function': 160 | default: 161 | return ( 162 | new DeclarationBlock({}) 163 | .export() 164 | .asKind('function') 165 | .withName(`${name}Schema(): z.ZodObject>`) 166 | .withBlock( 167 | [ 168 | indent(`return z.object({`), 169 | indent(`__typename: z.literal('${node.name.value}').optional(),`, 2), 170 | shape, 171 | indent('})'), 172 | ].join('\n'), 173 | ) 174 | .string + appendArguments 175 | ); 176 | } 177 | }), 178 | }; 179 | } 180 | 181 | get EnumTypeDefinition() { 182 | return { 183 | leave: (node: EnumTypeDefinitionNode) => { 184 | const visitor = this.createVisitor('both'); 185 | const enumname = visitor.convertName(node.name.value); 186 | const enumTypeName = visitor.prefixTypeNamespace(enumname); 187 | this.importTypes.push(enumname); 188 | 189 | // hoist enum declarations 190 | this.enumDeclarations.push( 191 | this.config.enumsAsTypes 192 | ? new DeclarationBlock({}) 193 | .export() 194 | .asKind('const') 195 | .withName(`${enumname}Schema`) 196 | .withContent(`z.enum([${node.values?.map(enumOption => `'${enumOption.name.value}'`).join(', ')}])`) 197 | .string 198 | : new DeclarationBlock({}) 199 | .export() 200 | .asKind('const') 201 | .withName(`${enumname}Schema`) 202 | .withContent(`z.nativeEnum(${enumTypeName})`) 203 | .string, 204 | ); 205 | }, 206 | }; 207 | } 208 | 209 | get UnionTypeDefinition() { 210 | return { 211 | leave: (node: UnionTypeDefinitionNode) => { 212 | if (!node.types || !this.config.withObjectType) 213 | return; 214 | const visitor = this.createVisitor('output'); 215 | const unionName = visitor.convertName(node.name.value); 216 | const unionElements = node.types.map((t) => { 217 | const element = visitor.convertName(t.name.value); 218 | const typ = visitor.getType(t.name.value); 219 | if (typ?.astNode?.kind === 'EnumTypeDefinition') 220 | return `${element}Schema`; 221 | 222 | switch (this.config.validationSchemaExportType) { 223 | case 'const': 224 | return `${element}Schema`; 225 | case 'function': 226 | default: 227 | return `${element}Schema()`; 228 | } 229 | }).join(', '); 230 | const unionElementsCount = node.types.length ?? 0; 231 | 232 | const union = unionElementsCount > 1 ? `z.union([${unionElements}])` : unionElements; 233 | 234 | switch (this.config.validationSchemaExportType) { 235 | case 'const': 236 | return new DeclarationBlock({}).export().asKind('const').withName(`${unionName}Schema`).withContent(union).string; 237 | case 'function': 238 | default: 239 | return new DeclarationBlock({}) 240 | .export() 241 | .asKind('function') 242 | .withName(`${unionName}Schema()`) 243 | .withBlock(indent(`return ${union}`)) 244 | .string; 245 | } 246 | }, 247 | }; 248 | } 249 | 250 | protected buildInputFields( 251 | fields: readonly (FieldDefinitionNode | InputValueDefinitionNode)[], 252 | visitor: Visitor, 253 | name: string, 254 | ) { 255 | const typeName = visitor.prefixTypeNamespace(name); 256 | const shape = fields.map(field => generateFieldZodSchema(this.config, visitor, field, 2)).join(',\n'); 257 | 258 | switch (this.config.validationSchemaExportType) { 259 | case 'const': 260 | return new DeclarationBlock({}) 261 | .export() 262 | .asKind('const') 263 | .withName(`${name}Schema: z.ZodObject>`) 264 | .withContent(['z.object({', shape, '})'].join('\n')) 265 | .string; 266 | 267 | case 'function': 268 | default: 269 | return new DeclarationBlock({}) 270 | .export() 271 | .asKind('function') 272 | .withName(`${name}Schema(): z.ZodObject>`) 273 | .withBlock([indent(`return z.object({`), shape, indent('})')].join('\n')) 274 | .string; 275 | } 276 | } 277 | } 278 | 279 | function generateFieldZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, indentCount: number): string { 280 | const gen = generateFieldTypeZodSchema(config, visitor, field, field.type); 281 | return indent(`${field.name.value}: ${maybeLazy(field.type, gen)}`, indentCount); 282 | } 283 | 284 | function generateFieldTypeZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, type: TypeNode, parentType?: TypeNode): string { 285 | if (isListType(type)) { 286 | const gen = generateFieldTypeZodSchema(config, visitor, field, type.type, type); 287 | if (!isNonNullType(parentType)) { 288 | const arrayGen = `z.array(${maybeLazy(type.type, gen)})`; 289 | const maybeLazyGen = applyDirectives(config, field, arrayGen); 290 | return `${maybeLazyGen}.nullish()`; 291 | } 292 | return `z.array(${maybeLazy(type.type, gen)})`; 293 | } 294 | if (isNonNullType(type)) { 295 | const gen = generateFieldTypeZodSchema(config, visitor, field, type.type, type); 296 | return maybeLazy(type.type, gen); 297 | } 298 | if (isNamedType(type)) { 299 | const gen = generateNameNodeZodSchema(config, visitor, type.name); 300 | if (isListType(parentType)) 301 | return `${gen}.nullable()`; 302 | 303 | let appliedDirectivesGen = applyDirectives(config, field, gen); 304 | 305 | if (field.kind === Kind.INPUT_VALUE_DEFINITION) { 306 | const { defaultValue } = field; 307 | 308 | if (defaultValue?.kind === Kind.INT || defaultValue?.kind === Kind.FLOAT || defaultValue?.kind === Kind.BOOLEAN) 309 | appliedDirectivesGen = `${appliedDirectivesGen}.default(${defaultValue.value})`; 310 | 311 | if (defaultValue?.kind === Kind.STRING || defaultValue?.kind === Kind.ENUM) { 312 | if (config.useEnumTypeAsDefaultValue && defaultValue?.kind !== Kind.STRING) { 313 | let value = convertNameParts(defaultValue.value, resolveExternalModuleAndFn('change-case-all#pascalCase'), config.namingConvention?.transformUnderscore); 314 | 315 | if (config.namingConvention?.enumValues) 316 | value = convertNameParts(defaultValue.value, resolveExternalModuleAndFn(config.namingConvention?.enumValues), config.namingConvention?.transformUnderscore); 317 | 318 | appliedDirectivesGen = `${appliedDirectivesGen}.default(${type.name.value}.${value})`; 319 | } 320 | else { 321 | appliedDirectivesGen = `${appliedDirectivesGen}.default("${escapeGraphQLCharacters(defaultValue.value)}")`; 322 | } 323 | } 324 | } 325 | 326 | if (isNonNullType(parentType)) { 327 | if (visitor.shouldEmitAsNotAllowEmptyString(type.name.value)) 328 | return `${appliedDirectivesGen}.min(1)`; 329 | 330 | return appliedDirectivesGen; 331 | } 332 | if (isListType(parentType)) 333 | return `${appliedDirectivesGen}.nullable()`; 334 | 335 | return `${appliedDirectivesGen}.nullish()`; 336 | } 337 | console.warn('unhandled type:', type); 338 | return ''; 339 | } 340 | 341 | function applyDirectives(config: ValidationSchemaPluginConfig, field: InputValueDefinitionNode | FieldDefinitionNode, gen: string): string { 342 | if (config.directives && field.directives) { 343 | const formatted = formatDirectiveConfig(config.directives); 344 | return gen + buildApi(formatted, field.directives); 345 | } 346 | return gen; 347 | } 348 | 349 | function generateNameNodeZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, node: NameNode): string { 350 | const converter = visitor.getNameNodeConverter(node); 351 | 352 | switch (converter?.targetKind) { 353 | case 'InterfaceTypeDefinition': 354 | case 'InputObjectTypeDefinition': 355 | case 'ObjectTypeDefinition': 356 | case 'UnionTypeDefinition': 357 | // using switch-case rather than if-else to allow for future expansion 358 | switch (config.validationSchemaExportType) { 359 | case 'const': 360 | return `${converter.convertName()}Schema`; 361 | case 'function': 362 | default: 363 | return `${converter.convertName()}Schema()`; 364 | } 365 | case 'EnumTypeDefinition': 366 | return `${converter.convertName()}Schema`; 367 | case 'ScalarTypeDefinition': 368 | return zod4Scalar(config, visitor, node.value); 369 | default: 370 | if (converter?.targetKind) 371 | console.warn('Unknown targetKind', converter?.targetKind); 372 | 373 | return zod4Scalar(config, visitor, node.value); 374 | } 375 | } 376 | 377 | function maybeLazy(type: TypeNode, schema: string): string { 378 | if (isNamedType(type) && isInput(type.name.value)) 379 | return `z.lazy(() => ${schema})`; 380 | 381 | return schema; 382 | } 383 | 384 | function zod4Scalar(config: ValidationSchemaPluginConfig, visitor: Visitor, scalarName: string): string { 385 | if (config.scalarSchemas?.[scalarName]) 386 | return config.scalarSchemas[scalarName]; 387 | 388 | const tsType = visitor.getScalarType(scalarName); 389 | switch (tsType) { 390 | case 'string': 391 | return `z.string()`; 392 | case 'number': 393 | return `z.number()`; 394 | case 'boolean': 395 | return `z.boolean()`; 396 | } 397 | 398 | if (config.defaultScalarTypeSchema) { 399 | return config.defaultScalarTypeSchema; 400 | } 401 | 402 | console.warn('unhandled scalar name:', scalarName); 403 | return anySchema; 404 | } 405 | -------------------------------------------------------------------------------- /tests/directive.spec.ts: -------------------------------------------------------------------------------- 1 | import type { ConstArgumentNode, ConstDirectiveNode, ConstValueNode, NameNode } from 'graphql'; 2 | import type { DirectiveConfig, DirectiveObjectArguments } from '../src/config'; 3 | import type { 4 | FormattedDirectiveArguments, 5 | FormattedDirectiveConfig, 6 | FormattedDirectiveObjectArguments, 7 | } from '../src/directive'; 8 | 9 | import { Kind, parseConstValue } from 'graphql'; 10 | import { 11 | buildApi, 12 | buildApiForValibot, 13 | exportedForTesting, 14 | formatDirectiveConfig, 15 | formatDirectiveObjectArguments, 16 | } from '../src/directive'; 17 | 18 | const { applyArgToApiSchemaTemplate, buildApiFromDirectiveObjectArguments, buildApiFromDirectiveArguments } 19 | = exportedForTesting; 20 | 21 | function buildNameNode(name: string): NameNode { 22 | return { 23 | kind: Kind.NAME, 24 | value: name, 25 | } 26 | } 27 | 28 | function buildConstArgumentNodes(args: Record): ConstArgumentNode[] { 29 | return Object.entries(args).map( 30 | ([argName, argValue]): ConstArgumentNode => ({ 31 | kind: Kind.ARGUMENT, 32 | name: buildNameNode(argName), 33 | value: parseConstValue(argValue), 34 | }), 35 | ) 36 | } 37 | 38 | function buildConstDirectiveNodes(name: string, args: Record): ConstDirectiveNode { 39 | return { 40 | kind: Kind.DIRECTIVE, 41 | name: buildNameNode(name), 42 | arguments: buildConstArgumentNodes(args), 43 | } 44 | } 45 | 46 | describe('format directive config', () => { 47 | describe('formatDirectiveObjectArguments', () => { 48 | const cases: { 49 | name: string 50 | arg: DirectiveObjectArguments 51 | want: FormattedDirectiveObjectArguments 52 | }[] = [ 53 | { 54 | name: 'normal', 55 | arg: { 56 | uri: 'url', 57 | email: 'email', 58 | }, 59 | want: { 60 | uri: ['url', '$2'], 61 | email: ['email', '$2'], 62 | }, 63 | }, 64 | { 65 | name: 'contains array', 66 | arg: { 67 | startWith: ['matches', '/^$2/'], 68 | email: 'email', 69 | }, 70 | want: { 71 | startWith: ['matches', '/^$2/'], 72 | email: ['email', '$2'], 73 | }, 74 | }, 75 | ]; 76 | for (const tc of cases) { 77 | it(tc.name, () => { 78 | const got = formatDirectiveObjectArguments(tc.arg); 79 | expect(got).toStrictEqual(tc.want); 80 | }); 81 | } 82 | }); 83 | 84 | describe('formatDirectiveConfig', () => { 85 | const cases: { 86 | name: string 87 | arg: DirectiveConfig 88 | want: FormattedDirectiveConfig 89 | }[] = [ 90 | { 91 | name: 'normal', 92 | arg: { 93 | required: { 94 | msg: 'required', 95 | }, 96 | constraint: { 97 | minLength: 'min', 98 | format: { 99 | uri: 'url', 100 | email: 'email', 101 | }, 102 | }, 103 | }, 104 | want: { 105 | required: { 106 | msg: ['required', '$1'], 107 | }, 108 | constraint: { 109 | minLength: ['min', '$1'], 110 | format: { 111 | uri: ['url', '$2'], 112 | email: ['email', '$2'], 113 | }, 114 | }, 115 | }, 116 | }, 117 | { 118 | name: 'complex', 119 | arg: { 120 | required: { 121 | msg: 'required', 122 | }, 123 | constraint: { 124 | startWith: ['matches', '/^$1/g'], 125 | format: { 126 | uri: ['url', '$2'], 127 | email: 'email', 128 | }, 129 | }, 130 | }, 131 | want: { 132 | required: { 133 | msg: ['required', '$1'], 134 | }, 135 | constraint: { 136 | startWith: ['matches', '/^$1/g'], 137 | format: { 138 | uri: ['url', '$2'], 139 | email: ['email', '$2'], 140 | }, 141 | }, 142 | }, 143 | }, 144 | ]; 145 | for (const tc of cases) { 146 | it(tc.name, () => { 147 | const got = formatDirectiveConfig(tc.arg); 148 | expect(got).toStrictEqual(tc.want); 149 | }); 150 | } 151 | }); 152 | 153 | describe('applyArgToApiSchemaTemplate', () => { 154 | const cases: { 155 | name: string 156 | args: { 157 | template: string 158 | apiArgs: any[] 159 | } 160 | want: string 161 | }[] = [ 162 | { 163 | name: 'string', 164 | args: { 165 | template: '$1', 166 | apiArgs: ['hello'], 167 | }, 168 | want: `"hello"`, 169 | }, 170 | { 171 | name: 'regexp string', 172 | args: { 173 | template: '$1', 174 | apiArgs: ['/hello/g'], 175 | }, 176 | want: `/hello/g`, 177 | }, 178 | { 179 | name: 'number', 180 | args: { 181 | template: '$1', 182 | apiArgs: [10], 183 | }, 184 | want: '10', 185 | }, 186 | { 187 | name: 'boolean', 188 | args: { 189 | template: '$1', 190 | apiArgs: [true], 191 | }, 192 | want: 'true', 193 | }, 194 | { 195 | name: 'eval number', 196 | args: { 197 | template: '$1', 198 | apiArgs: ['10 + 1'], 199 | }, 200 | want: '11', 201 | }, 202 | { 203 | name: 'eval boolean', 204 | args: { 205 | template: '$1', 206 | apiArgs: ['!true'], 207 | }, 208 | want: 'false', 209 | }, 210 | { 211 | name: 'eval template with number', 212 | args: { 213 | template: '$1 + 1', 214 | apiArgs: [10], 215 | }, 216 | want: '11', 217 | }, 218 | { 219 | name: 'eval template with boolean', 220 | args: { 221 | template: '$1 && false', 222 | apiArgs: [true], 223 | }, 224 | want: 'false', 225 | }, 226 | { 227 | name: 'array', 228 | args: { 229 | template: '$1', 230 | apiArgs: [['hello', 'world']], 231 | }, 232 | want: `"hello","world"`, 233 | }, 234 | { 235 | name: 'object', 236 | args: { 237 | template: '$1', 238 | apiArgs: [{ hello: 'world' }], 239 | }, 240 | want: `{"hello":"world"}`, 241 | }, 242 | { 243 | name: 'undefined', 244 | args: { 245 | template: '$1', 246 | apiArgs: ['undefined'], 247 | }, 248 | want: 'undefined', 249 | }, 250 | { 251 | name: 'null', 252 | args: { 253 | template: '$1', 254 | apiArgs: ['null'], 255 | }, 256 | want: 'null', 257 | }, 258 | { 259 | name: 'multiple', 260 | args: { 261 | template: '^$1|$2', 262 | apiArgs: ['hello', 'world'], 263 | }, 264 | want: `"^hello|world"`, 265 | }, 266 | { 267 | name: 'use only $2', 268 | args: { 269 | template: '$2$', 270 | apiArgs: ['hello', 'world'], 271 | }, 272 | want: `"world$"`, 273 | }, 274 | { 275 | name: 'does not match all', 276 | args: { 277 | template: '^$1', 278 | apiArgs: [], 279 | }, 280 | want: `"^"`, 281 | }, 282 | { 283 | name: 'if does not exists index', 284 | args: { 285 | template: '$1 $2 $3', 286 | apiArgs: ['hello', 'world'], 287 | }, 288 | want: `"hello world "`, 289 | }, 290 | ]; 291 | for (const tc of cases) { 292 | it(tc.name, () => { 293 | const { template, apiArgs } = tc.args; 294 | const got = applyArgToApiSchemaTemplate(template, apiArgs); 295 | expect(got).toBe(tc.want); 296 | }); 297 | } 298 | }); 299 | 300 | describe('buildApiFromDirectiveObjectArguments', () => { 301 | const cases: { 302 | name: string 303 | args: { 304 | config: FormattedDirectiveObjectArguments 305 | argValue: ConstValueNode 306 | } 307 | want: string 308 | }[] = [ 309 | { 310 | name: 'contains in config', 311 | args: { 312 | config: { 313 | uri: ['url', '$2'], 314 | }, 315 | argValue: parseConstValue(`"uri"`), 316 | }, 317 | want: `.url()`, 318 | }, 319 | { 320 | name: 'does not contains in config', 321 | args: { 322 | config: { 323 | email: ['email', '$2'], 324 | }, 325 | argValue: parseConstValue(`"uri"`), 326 | }, 327 | want: ``, 328 | }, 329 | { 330 | name: 'const value does not string type', 331 | args: { 332 | config: { 333 | email: ['email', '$2'], 334 | }, 335 | argValue: parseConstValue(`123`), 336 | }, 337 | want: ``, 338 | }, 339 | ]; 340 | for (const tc of cases) { 341 | it(tc.name, () => { 342 | const { config, argValue } = tc.args; 343 | const got = buildApiFromDirectiveObjectArguments(config, argValue); 344 | expect(got).toBe(tc.want); 345 | }); 346 | } 347 | }); 348 | 349 | describe('buildApiFromDirectiveArguments', () => { 350 | const cases: { 351 | name: string 352 | args: { 353 | config: FormattedDirectiveArguments 354 | args: ReadonlyArray 355 | } 356 | want: string 357 | }[] = [ 358 | { 359 | name: 'string', 360 | args: { 361 | config: { 362 | msg: ['required', '$1'], 363 | }, 364 | args: buildConstArgumentNodes({ 365 | msg: `"hello"`, 366 | }), 367 | }, 368 | want: `.required("hello")`, 369 | }, 370 | { 371 | name: 'string with additional stuff', 372 | args: { 373 | config: { 374 | startWith: ['matched', '^$1'], 375 | }, 376 | args: buildConstArgumentNodes({ 377 | startWith: `"hello"`, 378 | }), 379 | }, 380 | want: `.matched("^hello")`, 381 | }, 382 | { 383 | name: 'number', 384 | args: { 385 | config: { 386 | minLength: ['min', '$1'], 387 | }, 388 | args: buildConstArgumentNodes({ 389 | minLength: `1`, 390 | }), 391 | }, 392 | want: `.min(1)`, 393 | }, 394 | { 395 | name: 'boolean', 396 | args: { 397 | config: { 398 | // @strict(enabled: true) 399 | enabled: ['strict', '$1'], 400 | }, 401 | args: buildConstArgumentNodes({ 402 | enabled: `true`, 403 | }), 404 | }, 405 | want: `.strict(true)`, 406 | }, 407 | { 408 | name: 'list', 409 | args: { 410 | config: { 411 | minLength: ['min', '$1', '$2'], 412 | }, 413 | args: buildConstArgumentNodes({ 414 | minLength: `[1, "message"]`, 415 | }), 416 | }, 417 | want: `.min(1, "message")`, 418 | }, 419 | { 420 | name: 'object in list', 421 | args: { 422 | config: { 423 | matches: ['matches', '$1', '$2'], 424 | }, 425 | args: buildConstArgumentNodes({ 426 | matches: `["hello", {message:"message", excludeEmptyString:true}]`, 427 | }), 428 | }, 429 | want: `.matches("hello", {"message":"message","excludeEmptyString":true})`, 430 | }, 431 | { 432 | name: 'two arguments but matched to first argument', 433 | args: { 434 | config: { 435 | msg: ['required', '$1'], 436 | }, 437 | args: buildConstArgumentNodes({ 438 | msg: `"hello"`, 439 | msg2: `"world"`, 440 | }), 441 | }, 442 | want: `.required("hello")`, 443 | }, 444 | { 445 | name: 'two arguments but matched to second argument', 446 | args: { 447 | config: { 448 | msg2: ['required', '$1'], 449 | }, 450 | args: buildConstArgumentNodes({ 451 | msg: `"hello"`, 452 | msg2: `"world"`, 453 | }), 454 | }, 455 | want: `.required("world")`, 456 | }, 457 | { 458 | name: 'two arguments matched all', 459 | args: { 460 | config: { 461 | required: ['required', '$1'], 462 | minLength: ['min', '$1'], 463 | }, 464 | args: buildConstArgumentNodes({ 465 | required: `"message"`, 466 | minLength: `1`, 467 | }), 468 | }, 469 | want: `.required("message").min(1)`, 470 | }, 471 | { 472 | name: 'argument matches validation schema api', 473 | args: { 474 | config: { 475 | format: { 476 | uri: ['url'], 477 | }, 478 | }, 479 | args: buildConstArgumentNodes({ 480 | format: `"uri"`, 481 | }), 482 | }, 483 | want: `.url()`, 484 | }, 485 | { 486 | name: 'argument matched argument but doesn\'t match api', 487 | args: { 488 | config: { 489 | format: { 490 | uri: ['url'], 491 | }, 492 | }, 493 | args: buildConstArgumentNodes({ 494 | format: `"uuid"`, 495 | }), 496 | }, 497 | want: ``, 498 | }, 499 | { 500 | name: 'complex', 501 | args: { 502 | config: { 503 | required: ['required', '$1'], 504 | format: { 505 | uri: ['url'], 506 | }, 507 | }, 508 | args: buildConstArgumentNodes({ 509 | required: `"message"`, 510 | format: `"uri"`, 511 | }), 512 | }, 513 | want: `.required("message").url()`, 514 | }, 515 | { 516 | name: 'complex 2', 517 | args: { 518 | config: { 519 | required: ['required', '$1'], 520 | format: { 521 | uri: ['url'], 522 | }, 523 | }, 524 | args: buildConstArgumentNodes({ 525 | required: `"message"`, 526 | format: `"uuid"`, 527 | }), 528 | }, 529 | want: `.required("message")`, 530 | }, 531 | ]; 532 | for (const tc of cases) { 533 | it(tc.name, () => { 534 | const { config, args } = tc.args; 535 | const got = buildApiFromDirectiveArguments(config, args); 536 | expect(got).toStrictEqual(tc.want); 537 | }); 538 | } 539 | }); 540 | 541 | describe('buildApi', () => { 542 | const cases: { 543 | name: string 544 | args: { 545 | config: FormattedDirectiveConfig 546 | args: ReadonlyArray 547 | } 548 | want: string 549 | }[] = [ 550 | { 551 | name: 'valid', 552 | args: { 553 | config: { 554 | required: { 555 | msg: ['required', '$1'], 556 | }, 557 | constraint: { 558 | minLength: ['min', '$1'], 559 | format: { 560 | uri: ['url'], 561 | email: ['email'], 562 | }, 563 | }, 564 | }, 565 | args: [ 566 | // @required(msg: "message") 567 | buildConstDirectiveNodes('required', { 568 | msg: `"message"`, 569 | }), 570 | // @constraint(minLength: 100, format: "email") 571 | buildConstDirectiveNodes('constraint', { 572 | minLength: `100`, 573 | format: `"email"`, 574 | }), 575 | ], 576 | }, 577 | want: `.required("message").min(100).email()`, 578 | }, 579 | { 580 | name: 'enum', 581 | args: { 582 | config: { 583 | constraint: { 584 | format: { 585 | URI: ['uri'], 586 | }, 587 | }, 588 | }, 589 | args: [ 590 | // @constraint(format: EMAIL) 591 | buildConstDirectiveNodes('constraint', { 592 | format: 'URI', 593 | }), 594 | ], 595 | }, 596 | want: `.uri()`, 597 | }, 598 | ]; 599 | for (const tc of cases) { 600 | it(tc.name, () => { 601 | const { config, args } = tc.args; 602 | const got = buildApi(config, args); 603 | expect(got).toStrictEqual(tc.want); 604 | }); 605 | } 606 | }); 607 | 608 | describe('buildApiForValibot', () => { 609 | const cases: { 610 | name: string 611 | args: { 612 | config: FormattedDirectiveConfig 613 | args: ReadonlyArray 614 | } 615 | want: string[] 616 | }[] = [ 617 | { 618 | name: 'valid', 619 | args: { 620 | config: { 621 | constraint: { 622 | minLength: ['minLength', '$1'], 623 | format: { 624 | uri: ['url'], 625 | email: ['email'], 626 | }, 627 | }, 628 | }, 629 | args: [ 630 | // @constraint(minLength: 100, format: "email") 631 | buildConstDirectiveNodes('constraint', { 632 | minLength: `100`, 633 | format: `"email"`, 634 | }), 635 | ], 636 | }, 637 | want: [`v.minLength(100)`, `v.email()`], 638 | }, 639 | { 640 | name: 'enum', 641 | args: { 642 | config: { 643 | constraint: { 644 | format: { 645 | URI: ['uri'], 646 | }, 647 | }, 648 | }, 649 | args: [ 650 | // @constraint(format: EMAIL) 651 | buildConstDirectiveNodes('constraint', { 652 | format: 'URI', 653 | }), 654 | ], 655 | }, 656 | want: [`v.uri()`], 657 | }, 658 | ]; 659 | for (const tc of cases) { 660 | it(tc.name, () => { 661 | const { config, args } = tc.args; 662 | const got = buildApiForValibot(config, args); 663 | expect(got).toStrictEqual(tc.want); 664 | }); 665 | } 666 | }); 667 | }); 668 | -------------------------------------------------------------------------------- /tests/graphql.spec.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ObjectTypeDefinitionNode, 3 | } from 'graphql'; 4 | import { Graph } from 'graphlib'; 5 | import { 6 | buildClientSchema, 7 | buildSchema, 8 | introspectionFromSchema, 9 | Kind, 10 | parse, 11 | print, 12 | } from 'graphql'; 13 | import dedent from 'ts-dedent'; 14 | 15 | import { escapeGraphQLCharacters, isGeneratedByIntrospection, ObjectTypeDefinitionBuilder, topologicalSortAST, topsort } from '../src/graphql'; 16 | 17 | describe('graphql', () => { 18 | describe('objectTypeDefinitionBuilder', () => { 19 | describe('useObjectTypes === true', () => { 20 | it.each([ 21 | ['Query', false], 22 | ['Mutation', false], 23 | ['Subscription', false], 24 | ['QueryFoo', true], 25 | ['MutationFoo', true], 26 | ['SubscriptionFoo', true], 27 | ['FooQuery', true], 28 | ['FooMutation', true], 29 | ['FooSubscription', true], 30 | ['Foo', true], 31 | ])(`a node with a name of "%s" should be matched? %s`, (nodeName, nodeIsMatched) => { 32 | const node: ObjectTypeDefinitionNode = { 33 | name: { 34 | kind: Kind.NAME, 35 | value: nodeName, 36 | }, 37 | kind: Kind.OBJECT_TYPE_DEFINITION, 38 | }; 39 | 40 | const objectTypeDefFn = ObjectTypeDefinitionBuilder(true, (n: ObjectTypeDefinitionNode) => n); 41 | 42 | expect(objectTypeDefFn).toBeDefined(); 43 | 44 | if (nodeIsMatched) 45 | expect(objectTypeDefFn?.(node)).toBe(node); 46 | else 47 | expect(objectTypeDefFn?.(node)).toBeUndefined(); 48 | }); 49 | }); 50 | 51 | describe('useObjectTypes === false', () => { 52 | it('should not return an ObjectTypeDefinitionFn', () => { 53 | const objectTypeDefFn = ObjectTypeDefinitionBuilder(false, (n: ObjectTypeDefinitionNode) => n); 54 | expect(objectTypeDefFn).toBeUndefined(); 55 | }); 56 | }); 57 | }); 58 | }); 59 | 60 | describe('topsort', () => { 61 | it('should correctly sort nodes based on their dependencies', () => { 62 | const g = new Graph(); 63 | 64 | // Setting up the graph 65 | g.setNode('A'); 66 | g.setNode('B'); 67 | g.setNode('C'); 68 | g.setEdge('A', 'B'); 69 | g.setEdge('B', 'C'); 70 | 71 | const sortedNodes = topsort(g); 72 | expect(sortedNodes).toEqual(['C', 'B', 'A']); 73 | }); 74 | 75 | it('should correctly handle graph with no edges', () => { 76 | const g = new Graph(); 77 | 78 | // Setting up the graph 79 | g.setNode('A'); 80 | g.setNode('B'); 81 | g.setNode('C'); 82 | 83 | const sortedNodes = topsort(g); 84 | const isCorrectOrder = ['A', 'B', 'C'].every(node => sortedNodes.includes(node)); 85 | expect(isCorrectOrder).toBe(true); 86 | }); 87 | 88 | it('should correctly handle an empty graph', () => { 89 | const g = new Graph(); 90 | 91 | const sortedNodes = topsort(g); 92 | expect(sortedNodes).toEqual([]); 93 | }); 94 | 95 | it('should correctly handle graph with multiple dependencies', () => { 96 | const g = new Graph(); 97 | 98 | // Setting up the graph 99 | g.setNode('A'); 100 | g.setNode('B'); 101 | g.setNode('C'); 102 | g.setNode('D'); 103 | g.setEdge('A', 'B'); 104 | g.setEdge('A', 'C'); 105 | g.setEdge('B', 'D'); 106 | g.setEdge('C', 'D'); 107 | 108 | const sortedNodes = topsort(g); 109 | expect(sortedNodes).toEqual(['D', 'C', 'B', 'A']); 110 | }); 111 | }); 112 | 113 | describe('topologicalSortAST', () => { 114 | const getSortedSchema = (schema: string): string => { 115 | const ast = parse(schema); 116 | const sortedAst = topologicalSortAST(buildSchema(schema), ast); 117 | return print(sortedAst); 118 | }; 119 | 120 | it('should correctly sort nodes based on their input type dependencies', () => { 121 | const schema = /* GraphQL */ ` 122 | input A { 123 | b: B 124 | } 125 | 126 | input B { 127 | c: C 128 | } 129 | 130 | input C { 131 | d: String 132 | } 133 | `; 134 | 135 | const sortedSchema = getSortedSchema(schema); 136 | 137 | const expectedSortedSchema = dedent/* GraphQL */` 138 | input C { 139 | d: String 140 | } 141 | 142 | input B { 143 | c: C 144 | } 145 | 146 | input A { 147 | b: B 148 | } 149 | `; 150 | 151 | expect(sortedSchema).toBe(expectedSortedSchema); 152 | }); 153 | 154 | it('should correctly sort nodes based on their objet type dependencies', () => { 155 | const schema = /* GraphQL */ ` 156 | type D { 157 | e: UserKind 158 | } 159 | 160 | union UserKind = A | B 161 | 162 | type A { 163 | b: B 164 | } 165 | 166 | type B { 167 | c: C 168 | } 169 | 170 | type C { 171 | d: String 172 | } 173 | `; 174 | 175 | const sortedSchema = getSortedSchema(schema); 176 | 177 | const expectedSortedSchema = dedent/* GraphQL */` 178 | type C { 179 | d: String 180 | } 181 | 182 | type B { 183 | c: C 184 | } 185 | 186 | type A { 187 | b: B 188 | } 189 | 190 | union UserKind = A | B 191 | 192 | type D { 193 | e: UserKind 194 | } 195 | `; 196 | 197 | expect(sortedSchema).toBe(expectedSortedSchema); 198 | }); 199 | 200 | it('should place interface definitions before types that depend on them', () => { 201 | const schema = /* GraphQL */ ` 202 | type A { 203 | id: ID! 204 | node: Node 205 | } 206 | 207 | interface Node { 208 | id: ID! 209 | } 210 | `; 211 | 212 | const sortedSchema = getSortedSchema(schema); 213 | 214 | const expectedSortedSchema = dedent/* GraphQL */` 215 | interface Node { 216 | id: ID! 217 | } 218 | 219 | type A { 220 | id: ID! 221 | node: Node 222 | } 223 | `; 224 | 225 | expect(sortedSchema).toBe(expectedSortedSchema); 226 | }); 227 | 228 | it('should correctly handle schema with circular dependencies', () => { 229 | const schema = /* GraphQL */ ` 230 | input A { 231 | b: B 232 | } 233 | 234 | input B { 235 | a: A 236 | } 237 | `; 238 | const sortedSchema = getSortedSchema(schema); 239 | 240 | const expectedSortedSchema = dedent/* GraphQL */` 241 | input A { 242 | b: B 243 | } 244 | 245 | input B { 246 | a: A 247 | } 248 | `; 249 | 250 | expect(sortedSchema).toBe(expectedSortedSchema); 251 | }); 252 | 253 | it('should correctly handle schema with self circular dependencies', () => { 254 | const schema = /* GraphQL */ ` 255 | input A { 256 | a: A 257 | } 258 | 259 | input B { 260 | b: B 261 | } 262 | `; 263 | const sortedSchema = getSortedSchema(schema); 264 | 265 | const expectedSortedSchema = dedent/* GraphQL */` 266 | input B { 267 | b: B 268 | } 269 | 270 | input A { 271 | a: A 272 | } 273 | `; 274 | 275 | expect(sortedSchema).toBe(expectedSortedSchema); 276 | }); 277 | }); 278 | 279 | describe('isGeneratedByIntrospection function', () => { 280 | const schemaDefinition = /* GraphQL */ ` 281 | scalar CustomScalar 282 | 283 | interface Node { 284 | id: ID! 285 | } 286 | 287 | type UserType implements Node { 288 | id: ID! 289 | name: String! 290 | email: String! 291 | } 292 | 293 | union SearchResult = UserType 294 | 295 | enum Role { 296 | ADMIN 297 | USER 298 | } 299 | 300 | input UserInput { 301 | name: String! 302 | email: String! 303 | role: Role! 304 | } 305 | 306 | type Query { 307 | user(id: ID!): UserType! 308 | search(text: String!): [SearchResult] 309 | } 310 | 311 | type Mutation { 312 | createUser(input: UserInput!): UserType! 313 | } 314 | `; 315 | 316 | it('returns false for a schema not generated by introspection', () => { 317 | const schema = buildSchema(schemaDefinition); 318 | expect(isGeneratedByIntrospection(schema)).toBe(false); 319 | }); 320 | 321 | it('returns true for a schema generated by introspection', () => { 322 | const schema = buildSchema(schemaDefinition); 323 | const query = introspectionFromSchema(schema); 324 | const clientSchema = buildClientSchema(query); 325 | expect(isGeneratedByIntrospection(clientSchema)).toBe(true); 326 | }); 327 | }); 328 | 329 | describe('escapeGraphQLCharacters', () => { 330 | it('should escape double quotes', () => { 331 | const input = 'This is a "test" string.'; 332 | const expected = 'This is a \\\"test\\\" string.'; 333 | expect(escapeGraphQLCharacters(input)).toBe(expected); 334 | }); 335 | 336 | it('should escape backslashes', () => { 337 | const input = 'This is a backslash: \\'; 338 | const expected = 'This is a backslash: \\\\'; 339 | expect(escapeGraphQLCharacters(input)).toBe(expected); 340 | }); 341 | 342 | it('should escape forward slashes', () => { 343 | const input = 'This is a forward slash: /'; 344 | const expected = 'This is a forward slash: \\/'; 345 | expect(escapeGraphQLCharacters(input)).toBe(expected); 346 | }); 347 | 348 | it('should escape backspaces', () => { 349 | const input = 'This is a backspace: \b'; 350 | const expected = 'This is a backspace: \\b'; 351 | expect(escapeGraphQLCharacters(input)).toBe(expected); 352 | }); 353 | 354 | it('should escape form feeds', () => { 355 | const input = 'This is a form feed: \f'; 356 | const expected = 'This is a form feed: \\f'; 357 | expect(escapeGraphQLCharacters(input)).toBe(expected); 358 | }); 359 | 360 | it('should escape new lines', () => { 361 | const input = 'This is a new line: \n'; 362 | const expected = 'This is a new line: \\n'; 363 | expect(escapeGraphQLCharacters(input)).toBe(expected); 364 | }); 365 | 366 | it('should escape carriage returns', () => { 367 | const input = 'This is a carriage return: \r'; 368 | const expected = 'This is a carriage return: \\r'; 369 | expect(escapeGraphQLCharacters(input)).toBe(expected); 370 | }); 371 | 372 | it('should escape horizontal tabs', () => { 373 | const input = 'This is a tab: \t'; 374 | const expected = 'This is a tab: \\t'; 375 | expect(escapeGraphQLCharacters(input)).toBe(expected); 376 | }); 377 | 378 | it('should escape multiple special characters', () => { 379 | const input = 'This is a "test" string with \n new line and \t tab.'; 380 | const expected = 'This is a \\\"test\\\" string with \\n new line and \\t tab.'; 381 | expect(escapeGraphQLCharacters(input)).toBe(expected); 382 | }); 383 | 384 | it('should not escape non-special characters', () => { 385 | const input = 'Normal string with no special characters.'; 386 | const expected = 'Normal string with no special characters.'; 387 | expect(escapeGraphQLCharacters(input)).toBe(expected); 388 | }); 389 | }); 390 | -------------------------------------------------------------------------------- /tests/regexp.spec.ts: -------------------------------------------------------------------------------- 1 | import { isConvertableRegexp } from '../src/regexp'; 2 | 3 | describe('isConvertableRegexp', () => { 4 | describe('match', () => { 5 | it.each([ 6 | '//', 7 | '/hello/', 8 | '/hello/d', 9 | '/hello/g', 10 | '/hello/i', 11 | '/hello/m', 12 | '/hello/s', 13 | '/hello/u', 14 | '/hello/y', 15 | '/hello/dgimsuy', 16 | `/\\w+\\s/g`, 17 | 18 | `/^[a-z]+:[\\\/]$/i`, 19 | 20 | `/^(?:\d{3}|\(\d{3}\))([-\/\.])\d{3}\\1\d{4}$/`, 21 | ])('%s', (maybeRegexp) => { 22 | expect(isConvertableRegexp(maybeRegexp)).toBeTruthy(); 23 | }); 24 | }); 25 | describe('does not match', () => { 26 | it.each(['hello', '/world', 'world/', 'https://example.com/', ' /hello/', '/hello/d ', '/hello/dgimsuy '])( 27 | '%s', 28 | (maybeRegexp) => { 29 | expect(isConvertableRegexp(maybeRegexp)).toBeFalsy(); 30 | }, 31 | ); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /tests/valibot.spec.ts: -------------------------------------------------------------------------------- 1 | import { buildSchema } from 'graphql'; 2 | 3 | import { plugin } from '../src/index'; 4 | 5 | describe('valibot', () => { 6 | it('non-null and defined', async () => { 7 | const schema = buildSchema(/* GraphQL */ ` 8 | input PrimitiveInput { 9 | a: ID! 10 | b: String! 11 | c: Boolean! 12 | d: Int! 13 | e: Float! 14 | } 15 | `); 16 | const scalars = { 17 | ID: 'string', 18 | } 19 | const result = await plugin(schema, [], { schema: 'valibot', scalars }, {}); 20 | expect(result.content).toMatchInlineSnapshot(` 21 | " 22 | 23 | export function PrimitiveInputSchema(): v.GenericSchema { 24 | return v.object({ 25 | a: v.string(), 26 | b: v.string(), 27 | c: v.boolean(), 28 | d: v.number(), 29 | e: v.number() 30 | }) 31 | } 32 | " 33 | `); 34 | }) 35 | it('nullish', async () => { 36 | const schema = buildSchema(/* GraphQL */ ` 37 | input PrimitiveInput { 38 | a: ID 39 | b: String 40 | c: Boolean 41 | d: Int 42 | e: Float 43 | z: String! # no defined check 44 | } 45 | `); 46 | const scalars = { 47 | ID: 'string', 48 | } 49 | const result = await plugin(schema, [], { schema: 'valibot', scalars }, {}); 50 | expect(result.content).toMatchInlineSnapshot(` 51 | " 52 | 53 | export function PrimitiveInputSchema(): v.GenericSchema { 54 | return v.object({ 55 | a: v.nullish(v.string()), 56 | b: v.nullish(v.string()), 57 | c: v.nullish(v.boolean()), 58 | d: v.nullish(v.number()), 59 | e: v.nullish(v.number()), 60 | z: v.string() 61 | }) 62 | } 63 | " 64 | `); 65 | }) 66 | it('array', async () => { 67 | const schema = buildSchema(/* GraphQL */ ` 68 | input PrimitiveInput { 69 | a: [String] 70 | b: [String!] 71 | c: [String!]! 72 | d: [[String]] 73 | e: [[String]!] 74 | f: [[String]!]! 75 | } 76 | `); 77 | const scalars = undefined 78 | const result = await plugin(schema, [], { schema: 'valibot', scalars }, {}); 79 | expect(result.content).toMatchInlineSnapshot(` 80 | " 81 | 82 | export function PrimitiveInputSchema(): v.GenericSchema { 83 | return v.object({ 84 | a: v.nullish(v.array(v.nullable(v.string()))), 85 | b: v.nullish(v.array(v.string())), 86 | c: v.array(v.string()), 87 | d: v.nullish(v.array(v.nullish(v.array(v.nullable(v.string()))))), 88 | e: v.nullish(v.array(v.array(v.nullable(v.string())))), 89 | f: v.array(v.array(v.nullable(v.string()))) 90 | }) 91 | } 92 | " 93 | `); 94 | }) 95 | it('ref input object', async () => { 96 | const schema = buildSchema(/* GraphQL */ ` 97 | input AInput { 98 | b: BInput! 99 | } 100 | input BInput { 101 | c: CInput! 102 | } 103 | input CInput { 104 | a: AInput! 105 | } 106 | `); 107 | const scalars = undefined 108 | const result = await plugin(schema, [], { schema: 'valibot', scalars }, {}); 109 | expect(result.content).toMatchInlineSnapshot(` 110 | " 111 | 112 | export function AInputSchema(): v.GenericSchema { 113 | return v.object({ 114 | b: v.lazy(() => BInputSchema()) 115 | }) 116 | } 117 | 118 | export function BInputSchema(): v.GenericSchema { 119 | return v.object({ 120 | c: v.lazy(() => CInputSchema()) 121 | }) 122 | } 123 | 124 | export function CInputSchema(): v.GenericSchema { 125 | return v.object({ 126 | a: v.lazy(() => AInputSchema()) 127 | }) 128 | } 129 | " 130 | `); 131 | }) 132 | it('ref input object w/ schemaNamespacedImportName', async () => { 133 | const schema = buildSchema(/* GraphQL */ ` 134 | input AInput { 135 | b: BInput! 136 | } 137 | input BInput { 138 | c: CInput! 139 | } 140 | input CInput { 141 | a: AInput! 142 | } 143 | `); 144 | const scalars = undefined 145 | const result = await plugin(schema, [], { schema: 'valibot', scalars, importFrom: './types', schemaNamespacedImportName: 't' }, {}); 146 | expect(result.content).toMatchInlineSnapshot(` 147 | " 148 | 149 | export function AInputSchema(): v.GenericSchema { 150 | return v.object({ 151 | b: v.lazy(() => BInputSchema()) 152 | }) 153 | } 154 | 155 | export function BInputSchema(): v.GenericSchema { 156 | return v.object({ 157 | c: v.lazy(() => CInputSchema()) 158 | }) 159 | } 160 | 161 | export function CInputSchema(): v.GenericSchema { 162 | return v.object({ 163 | a: v.lazy(() => AInputSchema()) 164 | }) 165 | } 166 | " 167 | `); 168 | }) 169 | it('nested input object', async () => { 170 | const schema = buildSchema(/* GraphQL */ ` 171 | input NestedInput { 172 | child: NestedInput 173 | childrens: [NestedInput] 174 | } 175 | `); 176 | const scalars = undefined 177 | const result = await plugin(schema, [], { schema: 'valibot', scalars }, {}); 178 | 179 | expect(result.content).toMatchInlineSnapshot(` 180 | " 181 | 182 | export function NestedInputSchema(): v.GenericSchema { 183 | return v.object({ 184 | child: v.lazy(() => v.nullish(NestedInputSchema())), 185 | childrens: v.nullish(v.array(v.lazy(() => v.nullable(NestedInputSchema())))) 186 | }) 187 | } 188 | " 189 | `) 190 | }) 191 | it('enum', async () => { 192 | const schema = buildSchema(/* GraphQL */ ` 193 | enum PageType { 194 | PUBLIC 195 | BASIC_AUTH 196 | } 197 | input PageInput { 198 | pageType: PageType! 199 | } 200 | `); 201 | const scalars = undefined 202 | const result = await plugin(schema, [], { schema: 'valibot', scalars }, {}); 203 | expect(result.content).toMatchInlineSnapshot(` 204 | " 205 | export const PageTypeSchema = v.enum_(PageType); 206 | 207 | export function PageInputSchema(): v.GenericSchema { 208 | return v.object({ 209 | pageType: PageTypeSchema 210 | }) 211 | } 212 | " 213 | `); 214 | }) 215 | it('enum w/ schemaNamespacedImportName', async () => { 216 | const schema = buildSchema(/* GraphQL */ ` 217 | enum PageType { 218 | PUBLIC 219 | BASIC_AUTH 220 | } 221 | input PageInput { 222 | pageType: PageType! 223 | } 224 | `); 225 | const scalars = undefined 226 | const result = await plugin(schema, [], { schema: 'valibot', scalars, importFrom: './types', schemaNamespacedImportName: 't' }, {}); 227 | expect(result.content).toMatchInlineSnapshot(` 228 | " 229 | export const PageTypeSchema = v.enum_(t.PageType); 230 | 231 | export function PageInputSchema(): v.GenericSchema { 232 | return v.object({ 233 | pageType: PageTypeSchema 234 | }) 235 | } 236 | " 237 | `); 238 | }) 239 | it('camelcase', async () => { 240 | const schema = buildSchema(/* GraphQL */ ` 241 | input HTTPInput { 242 | method: HTTPMethod 243 | url: URL! 244 | } 245 | enum HTTPMethod { 246 | GET 247 | POST 248 | } 249 | scalar URL # unknown scalar, should be any 250 | `); 251 | const scalars = undefined 252 | const result = await plugin(schema, [], { schema: 'valibot', scalars }, {}); 253 | expect(result.content).toMatchInlineSnapshot(` 254 | " 255 | export const HttpMethodSchema = v.enum_(HttpMethod); 256 | 257 | export function HttpInputSchema(): v.GenericSchema { 258 | return v.object({ 259 | method: v.nullish(HttpMethodSchema), 260 | url: v.any() 261 | }) 262 | } 263 | " 264 | `); 265 | }) 266 | it('with scalars', async () => { 267 | const schema = buildSchema(/* GraphQL */ ` 268 | input Say { 269 | phrase: Text! 270 | times: Count! 271 | } 272 | scalar Count 273 | scalar Text 274 | `); 275 | const result = await plugin( 276 | schema, 277 | [], 278 | { 279 | schema: 'valibot', 280 | scalars: { 281 | Text: 'string', 282 | Count: 'number', 283 | }, 284 | }, 285 | {}, 286 | ); 287 | expect(result.content).toMatchInlineSnapshot(` 288 | " 289 | 290 | export function SaySchema(): v.GenericSchema { 291 | return v.object({ 292 | phrase: v.string(), 293 | times: v.number() 294 | }) 295 | } 296 | " 297 | `); 298 | }); 299 | it('with importFrom', async () => { 300 | const schema = buildSchema(/* GraphQL */ ` 301 | input Say { 302 | phrase: String! 303 | } 304 | `); 305 | const result = await plugin( 306 | schema, 307 | [], 308 | { 309 | schema: 'valibot', 310 | importFrom: './types', 311 | }, 312 | {}, 313 | ); 314 | expect(result.prepend).toMatchInlineSnapshot(` 315 | [ 316 | "import * as v from 'valibot'", 317 | "import { Say } from './types'", 318 | ] 319 | `); 320 | expect(result.content).toMatchInlineSnapshot(` 321 | " 322 | 323 | export function SaySchema(): v.GenericSchema { 324 | return v.object({ 325 | phrase: v.string() 326 | }) 327 | } 328 | " 329 | `); 330 | }); 331 | it('with importFrom & schemaNamespacedImportName', async () => { 332 | const schema = buildSchema(/* GraphQL */ ` 333 | input Say { 334 | phrase: String! 335 | } 336 | `); 337 | const result = await plugin( 338 | schema, 339 | [], 340 | { 341 | schema: 'valibot', 342 | importFrom: './types', 343 | schemaNamespacedImportName: 't', 344 | }, 345 | {}, 346 | ); 347 | expect(result.prepend).toMatchInlineSnapshot(` 348 | [ 349 | "import * as v from 'valibot'", 350 | "import * as t from './types'", 351 | ] 352 | `); 353 | expect(result.content).toMatchInlineSnapshot(` 354 | " 355 | 356 | export function SaySchema(): v.GenericSchema { 357 | return v.object({ 358 | phrase: v.string() 359 | }) 360 | } 361 | " 362 | `); 363 | }); 364 | it('with importFrom & useTypeImports', async () => { 365 | const schema = buildSchema(/* GraphQL */ ` 366 | input Say { 367 | phrase: String! 368 | } 369 | `); 370 | const result = await plugin( 371 | schema, 372 | [], 373 | { 374 | schema: 'valibot', 375 | importFrom: './types', 376 | useTypeImports: true, 377 | }, 378 | {}, 379 | ); 380 | expect(result.prepend).toMatchInlineSnapshot(` 381 | [ 382 | "import * as v from 'valibot'", 383 | "import type { Say } from './types'", 384 | ] 385 | `); 386 | expect(result.content).toMatchInlineSnapshot(` 387 | " 388 | 389 | export function SaySchema(): v.GenericSchema { 390 | return v.object({ 391 | phrase: v.string() 392 | }) 393 | } 394 | " 395 | `); 396 | }); 397 | it('with enumsAsTypes', async () => { 398 | const schema = buildSchema(/* GraphQL */ ` 399 | enum PageType { 400 | PUBLIC 401 | BASIC_AUTH 402 | } 403 | `); 404 | const result = await plugin( 405 | schema, 406 | [], 407 | { 408 | schema: 'valibot', 409 | enumsAsTypes: true, 410 | }, 411 | {}, 412 | ); 413 | expect(result.content).toMatchInlineSnapshot(` 414 | " 415 | export const PageTypeSchema = v.picklist([\'PUBLIC\', \'BASIC_AUTH\']); 416 | " 417 | `); 418 | }); 419 | it('with enumsAsTypes & schemaNamespacedImportName', async () => { 420 | const schema = buildSchema(/* GraphQL */ ` 421 | enum PageType { 422 | PUBLIC 423 | BASIC_AUTH 424 | } 425 | `); 426 | const result = await plugin( 427 | schema, 428 | [], 429 | { 430 | schema: 'valibot', 431 | enumsAsTypes: true, 432 | importFrom: './types', 433 | schemaNamespacedImportName: 't', 434 | }, 435 | {}, 436 | ); 437 | expect(result.content).toMatchInlineSnapshot(` 438 | " 439 | export const PageTypeSchema = v.picklist([\'PUBLIC\', \'BASIC_AUTH\']); 440 | " 441 | `); 442 | }); 443 | it('with notAllowEmptyString', async () => { 444 | const schema = buildSchema(/* GraphQL */ ` 445 | input PrimitiveInput { 446 | a: ID! 447 | b: String! 448 | c: Boolean! 449 | d: Int! 450 | e: Float! 451 | } 452 | `); 453 | const result = await plugin( 454 | schema, 455 | [], 456 | { 457 | schema: 'valibot', 458 | notAllowEmptyString: true, 459 | scalars: { 460 | ID: 'string', 461 | }, 462 | }, 463 | {}, 464 | ); 465 | expect(result.content).toMatchInlineSnapshot(` 466 | " 467 | 468 | export function PrimitiveInputSchema(): v.GenericSchema { 469 | return v.object({ 470 | a: v.pipe(v.string(), v.minLength(1)), 471 | b: v.pipe(v.string(), v.minLength(1)), 472 | c: v.boolean(), 473 | d: v.number(), 474 | e: v.number() 475 | }) 476 | } 477 | " 478 | `) 479 | }) 480 | it('with notAllowEmptyString issue #386', async () => { 481 | const schema = buildSchema(/* GraphQL */ ` 482 | input InputOne { 483 | field: InputNested! 484 | } 485 | 486 | input InputNested { 487 | field: String! 488 | } 489 | `); 490 | const result = await plugin( 491 | schema, 492 | [], 493 | { 494 | schema: 'valibot', 495 | notAllowEmptyString: true, 496 | scalars: { 497 | ID: 'string', 498 | }, 499 | }, 500 | {}, 501 | ); 502 | expect(result.content).toMatchInlineSnapshot(` 503 | " 504 | 505 | export function InputOneSchema(): v.GenericSchema { 506 | return v.object({ 507 | field: v.lazy(() => InputNestedSchema()) 508 | }) 509 | } 510 | 511 | export function InputNestedSchema(): v.GenericSchema { 512 | return v.object({ 513 | field: v.pipe(v.string(), v.minLength(1)) 514 | }) 515 | } 516 | " 517 | `) 518 | }) 519 | it('with scalarSchemas', async () => { 520 | const schema = buildSchema(/* GraphQL */ ` 521 | input ScalarsInput { 522 | date: Date! 523 | email: Email 524 | str: String! 525 | } 526 | scalar Date 527 | scalar Email 528 | `); 529 | const result = await plugin( 530 | schema, 531 | [], 532 | { 533 | schema: 'valibot', 534 | scalarSchemas: { 535 | Date: 'v.date()', 536 | Email: 'v.string([v.email()])', 537 | }, 538 | }, 539 | {}, 540 | ); 541 | expect(result.content).toMatchInlineSnapshot(` 542 | " 543 | 544 | export function ScalarsInputSchema(): v.GenericSchema { 545 | return v.object({ 546 | date: v.date(), 547 | email: v.nullish(v.string([v.email()])), 548 | str: v.string() 549 | }) 550 | } 551 | " 552 | `) 553 | }); 554 | 555 | it('with defaultScalarTypeSchema', async () => { 556 | const schema = buildSchema(/* GraphQL */ ` 557 | input ScalarsInput { 558 | date: Date! 559 | email: Email 560 | str: String! 561 | } 562 | scalar Date 563 | scalar Email 564 | `); 565 | const result = await plugin( 566 | schema, 567 | [], 568 | { 569 | schema: 'valibot', 570 | scalarSchemas: { 571 | Email: 'v.string([v.email()])', 572 | }, 573 | defaultScalarTypeSchema: 'v.string()', 574 | }, 575 | {}, 576 | ); 577 | expect(result.content).toMatchInlineSnapshot(` 578 | " 579 | 580 | export function ScalarsInputSchema(): v.GenericSchema { 581 | return v.object({ 582 | date: v.string(), 583 | email: v.nullish(v.string([v.email()])), 584 | str: v.string() 585 | }) 586 | } 587 | " 588 | `) 589 | }); 590 | 591 | it('with typesPrefix', async () => { 592 | const schema = buildSchema(/* GraphQL */ ` 593 | input Say { 594 | phrase: String! 595 | } 596 | `); 597 | const result = await plugin( 598 | schema, 599 | [], 600 | { 601 | schema: 'valibot', 602 | typesPrefix: 'I', 603 | importFrom: './types', 604 | }, 605 | {}, 606 | ); 607 | expect(result.prepend).toMatchInlineSnapshot(` 608 | [ 609 | "import * as v from 'valibot'", 610 | "import { ISay } from './types'", 611 | ] 612 | `) 613 | expect(result.content).toMatchInlineSnapshot(` 614 | " 615 | 616 | export function ISaySchema(): v.GenericSchema { 617 | return v.object({ 618 | phrase: v.string() 619 | }) 620 | } 621 | " 622 | `) 623 | }) 624 | it('with typesSuffix', async () => { 625 | const schema = buildSchema(/* GraphQL */ ` 626 | input Say { 627 | phrase: String! 628 | } 629 | `); 630 | const result = await plugin( 631 | schema, 632 | [], 633 | { 634 | schema: 'valibot', 635 | typesSuffix: 'I', 636 | importFrom: './types', 637 | }, 638 | {}, 639 | ); 640 | expect(result.prepend).toMatchInlineSnapshot(` 641 | [ 642 | "import * as v from 'valibot'", 643 | "import { SayI } from './types'", 644 | ] 645 | `) 646 | expect(result.content).toMatchInlineSnapshot(` 647 | " 648 | 649 | export function SayISchema(): v.GenericSchema { 650 | return v.object({ 651 | phrase: v.string() 652 | }) 653 | } 654 | " 655 | `) 656 | }) 657 | it.todo('with default input values') 658 | describe('issues #19', () => { 659 | it('string field', async () => { 660 | const schema = buildSchema(/* GraphQL */ ` 661 | input UserCreateInput { 662 | profile: String @constraint(minLength: 1, maxLength: 5000) 663 | } 664 | directive @constraint(minLength: Int!, maxLength: Int!) on INPUT_FIELD_DEFINITION 665 | `); 666 | const result = await plugin( 667 | schema, 668 | [], 669 | { 670 | schema: 'valibot', 671 | directives: { 672 | constraint: { 673 | minLength: ['minLength', '$1', 'Please input more than $1'], 674 | maxLength: ['maxLength', '$1', 'Please input less than $1'], 675 | }, 676 | }, 677 | }, 678 | {}, 679 | ); 680 | expect(result.content).toMatchInlineSnapshot(` 681 | " 682 | 683 | export function UserCreateInputSchema(): v.GenericSchema { 684 | return v.object({ 685 | profile: v.nullish(v.pipe(v.string(), v.minLength(1, "Please input more than 1"), v.maxLength(5000, "Please input less than 5000"))) 686 | }) 687 | } 688 | " 689 | `) 690 | }); 691 | 692 | it('not null field', async () => { 693 | const schema = buildSchema(/* GraphQL */ ` 694 | input UserCreateInput { 695 | profile: String! @constraint(minLength: 1, maxLength: 5000) 696 | } 697 | directive @constraint(minLength: Int!, maxLength: Int!) on INPUT_FIELD_DEFINITION 698 | `); 699 | const result = await plugin( 700 | schema, 701 | [], 702 | { 703 | schema: 'valibot', 704 | directives: { 705 | constraint: { 706 | minLength: ['minLength', '$1', 'Please input more than $1'], 707 | maxLength: ['maxLength', '$1', 'Please input less than $1'], 708 | }, 709 | }, 710 | }, 711 | {}, 712 | ); 713 | 714 | expect(result.content).toMatchInlineSnapshot(` 715 | " 716 | 717 | export function UserCreateInputSchema(): v.GenericSchema { 718 | return v.object({ 719 | profile: v.pipe(v.string(), v.minLength(1, "Please input more than 1"), v.maxLength(5000, "Please input less than 5000")) 720 | }) 721 | } 722 | " 723 | `) 724 | }); 725 | it.todo('list field') 726 | describe('pR #112', () => { 727 | it.todo('with notAllowEmptyString') 728 | it.todo('without notAllowEmptyString') 729 | }) 730 | describe('with withObjectType', () => { 731 | it('not generate if withObjectType false', async () => { 732 | const schema = buildSchema(/* GraphQL */ ` 733 | type User { 734 | id: ID! 735 | name: String 736 | } 737 | `); 738 | const result = await plugin( 739 | schema, 740 | [], 741 | { 742 | schema: 'valibot', 743 | }, 744 | {}, 745 | ); 746 | expect(result.content).not.toContain('export function UserSchema(): v.GenericSchema'); 747 | }); 748 | it('generate object type contains object type', async () => { 749 | const schema = buildSchema(/* GraphQL */ ` 750 | type Book { 751 | author: Author 752 | title: String 753 | } 754 | 755 | type Author { 756 | books: [Book] 757 | name: String 758 | } 759 | `); 760 | const result = await plugin( 761 | schema, 762 | [], 763 | { 764 | schema: 'valibot', 765 | withObjectType: true, 766 | }, 767 | {}, 768 | ); 769 | expect(result.content).toMatchInlineSnapshot(` 770 | " 771 | 772 | export function BookSchema(): v.GenericSchema { 773 | return v.object({ 774 | __typename: v.optional(v.literal('Book')), 775 | author: v.nullish(AuthorSchema()), 776 | title: v.nullish(v.string()) 777 | }) 778 | } 779 | 780 | export function AuthorSchema(): v.GenericSchema { 781 | return v.object({ 782 | __typename: v.optional(v.literal('Author')), 783 | books: v.nullish(v.array(v.nullable(BookSchema()))), 784 | name: v.nullish(v.string()) 785 | }) 786 | } 787 | " 788 | `) 789 | 790 | for (const wantNotContain of ['Query', 'Mutation', 'Subscription']) 791 | expect(result.content).not.toContain(wantNotContain); 792 | }); 793 | it('generate both input & type', async () => { 794 | const schema = buildSchema(/* GraphQL */ ` 795 | scalar Date 796 | scalar Email 797 | input UserCreateInput { 798 | name: String! 799 | date: Date! 800 | email: Email! 801 | } 802 | input UsernameUpdateInput { 803 | updateInputId: ID! 804 | updateName: String! 805 | } 806 | type User { 807 | id: ID! 808 | name: String 809 | age: Int 810 | email: Email 811 | isMember: Boolean 812 | createdAt: Date! 813 | } 814 | 815 | type Mutation { 816 | _empty: String 817 | } 818 | 819 | type Query { 820 | _empty: String 821 | } 822 | 823 | type Subscription { 824 | _empty: String 825 | } 826 | `); 827 | const result = await plugin( 828 | schema, 829 | [], 830 | { 831 | schema: 'valibot', 832 | withObjectType: true, 833 | scalarSchemas: { 834 | Date: 'v.date()', 835 | Email: 'v.pipe(v.string(), v.email())', 836 | }, 837 | scalars: { 838 | ID: { 839 | input: 'number', 840 | output: 'string', 841 | }, 842 | }, 843 | }, 844 | {}, 845 | ); 846 | expect(result.content).toMatchInlineSnapshot(` 847 | " 848 | 849 | export function UserCreateInputSchema(): v.GenericSchema { 850 | return v.object({ 851 | name: v.string(), 852 | date: v.date(), 853 | email: v.pipe(v.string(), v.email()) 854 | }) 855 | } 856 | 857 | export function UsernameUpdateInputSchema(): v.GenericSchema { 858 | return v.object({ 859 | updateInputId: v.number(), 860 | updateName: v.string() 861 | }) 862 | } 863 | 864 | export function UserSchema(): v.GenericSchema { 865 | return v.object({ 866 | __typename: v.optional(v.literal('User')), 867 | id: v.string(), 868 | name: v.nullish(v.string()), 869 | age: v.nullish(v.number()), 870 | email: v.nullish(v.pipe(v.string(), v.email())), 871 | isMember: v.nullish(v.boolean()), 872 | createdAt: v.date() 873 | }) 874 | } 875 | " 876 | `) 877 | 878 | for (const wantNotContain of ['Query', 'Mutation', 'Subscription']) 879 | expect(result.content).not.toContain(wantNotContain); 880 | }); 881 | }) 882 | it('generate union types', async () => { 883 | const schema = buildSchema(/* GraphQL */ ` 884 | type Square { 885 | size: Int 886 | } 887 | type Circle { 888 | radius: Int 889 | } 890 | union Shape = Circle | Square 891 | `); 892 | 893 | const result = await plugin( 894 | schema, 895 | [], 896 | { 897 | schema: 'valibot', 898 | withObjectType: true, 899 | }, 900 | {}, 901 | ); 902 | 903 | expect(result.content).toMatchInlineSnapshot(` 904 | " 905 | 906 | export function SquareSchema(): v.GenericSchema { 907 | return v.object({ 908 | __typename: v.optional(v.literal('Square')), 909 | size: v.nullish(v.number()) 910 | }) 911 | } 912 | 913 | export function CircleSchema(): v.GenericSchema { 914 | return v.object({ 915 | __typename: v.optional(v.literal('Circle')), 916 | radius: v.nullish(v.number()) 917 | }) 918 | } 919 | 920 | export function ShapeSchema() { 921 | return v.union([CircleSchema(), SquareSchema()]) 922 | } 923 | " 924 | `) 925 | }); 926 | }) 927 | it('generate union types & schemaNamespacedImportName', async () => { 928 | const schema = buildSchema(/* GraphQL */ ` 929 | type Square { 930 | size: Int 931 | } 932 | type Circle { 933 | radius: Int 934 | } 935 | union Shape = Circle | Square 936 | `); 937 | 938 | const result = await plugin( 939 | schema, 940 | [], 941 | { 942 | schema: 'valibot', 943 | withObjectType: true, 944 | importFrom: './types', 945 | schemaNamespacedImportName: 't', 946 | }, 947 | {}, 948 | ); 949 | 950 | expect(result.content).toMatchInlineSnapshot(` 951 | " 952 | 953 | export function SquareSchema(): v.GenericSchema { 954 | return v.object({ 955 | __typename: v.optional(v.literal('Square')), 956 | size: v.nullish(v.number()) 957 | }) 958 | } 959 | 960 | export function CircleSchema(): v.GenericSchema { 961 | return v.object({ 962 | __typename: v.optional(v.literal('Circle')), 963 | radius: v.nullish(v.number()) 964 | }) 965 | } 966 | 967 | export function ShapeSchema() { 968 | return v.union([CircleSchema(), SquareSchema()]) 969 | } 970 | " 971 | `) 972 | }); 973 | it('generate union types with single element', async () => { 974 | const schema = buildSchema(/* GraphQL */ ` 975 | type Square { 976 | size: Int 977 | } 978 | type Circle { 979 | radius: Int 980 | } 981 | union Shape = Circle | Square 982 | 983 | type Geometry { 984 | shape: Shape 985 | } 986 | `); 987 | 988 | const result = await plugin( 989 | schema, 990 | [], 991 | { 992 | schema: 'valibot', 993 | withObjectType: true, 994 | }, 995 | {}, 996 | ); 997 | 998 | expect(result.content).toMatchInlineSnapshot(` 999 | " 1000 | 1001 | export function SquareSchema(): v.GenericSchema { 1002 | return v.object({ 1003 | __typename: v.optional(v.literal('Square')), 1004 | size: v.nullish(v.number()) 1005 | }) 1006 | } 1007 | 1008 | export function CircleSchema(): v.GenericSchema { 1009 | return v.object({ 1010 | __typename: v.optional(v.literal('Circle')), 1011 | radius: v.nullish(v.number()) 1012 | }) 1013 | } 1014 | 1015 | export function ShapeSchema() { 1016 | return v.union([CircleSchema(), SquareSchema()]) 1017 | } 1018 | 1019 | export function GeometrySchema(): v.GenericSchema { 1020 | return v.object({ 1021 | __typename: v.optional(v.literal('Geometry')), 1022 | shape: v.nullish(ShapeSchema()) 1023 | }) 1024 | } 1025 | " 1026 | `) 1027 | }); 1028 | it('correctly reference generated union types', async () => { 1029 | const schema = buildSchema(/* GraphQL */ ` 1030 | type Circle { 1031 | radius: Int 1032 | } 1033 | union Shape = Circle 1034 | `); 1035 | 1036 | const result = await plugin( 1037 | schema, 1038 | [], 1039 | { 1040 | schema: 'valibot', 1041 | withObjectType: true, 1042 | }, 1043 | {}, 1044 | ); 1045 | 1046 | expect(result.content).toMatchInlineSnapshot(` 1047 | " 1048 | 1049 | export function CircleSchema(): v.GenericSchema { 1050 | return v.object({ 1051 | __typename: v.optional(v.literal('Circle')), 1052 | radius: v.nullish(v.number()) 1053 | }) 1054 | } 1055 | 1056 | export function ShapeSchema() { 1057 | return CircleSchema() 1058 | } 1059 | " 1060 | `) 1061 | }); 1062 | it('generate enum union types', async () => { 1063 | const schema = buildSchema(/* GraphQL */ ` 1064 | enum PageType { 1065 | PUBLIC 1066 | BASIC_AUTH 1067 | } 1068 | 1069 | enum MethodType { 1070 | GET 1071 | POST 1072 | } 1073 | 1074 | union AnyType = PageType | MethodType 1075 | `); 1076 | 1077 | const result = await plugin( 1078 | schema, 1079 | [], 1080 | { 1081 | schema: 'valibot', 1082 | withObjectType: true, 1083 | }, 1084 | {}, 1085 | ); 1086 | 1087 | expect(result.content).toMatchInlineSnapshot(` 1088 | " 1089 | export const PageTypeSchema = v.enum_(PageType); 1090 | 1091 | export const MethodTypeSchema = v.enum_(MethodType); 1092 | 1093 | export function AnyTypeSchema() { 1094 | return v.union([PageTypeSchema, MethodTypeSchema]) 1095 | } 1096 | " 1097 | `) 1098 | }); 1099 | it.todo('generate union types with single element, export as const') 1100 | it('with object arguments', async () => { 1101 | const schema = buildSchema(/* GraphQL */ ` 1102 | type MyType { 1103 | foo(a: String, b: Int!, c: Boolean, d: Float!, e: Text): String 1104 | } 1105 | scalar Text 1106 | `); 1107 | const result = await plugin( 1108 | schema, 1109 | [], 1110 | { 1111 | schema: 'valibot', 1112 | withObjectType: true, 1113 | scalars: { 1114 | Text: 'string', 1115 | }, 1116 | }, 1117 | {}, 1118 | ); 1119 | expect(result.content).toMatchInlineSnapshot(` 1120 | " 1121 | 1122 | export function MyTypeSchema(): v.GenericSchema { 1123 | return v.object({ 1124 | __typename: v.optional(v.literal('MyType')), 1125 | foo: v.nullish(v.string()) 1126 | }) 1127 | } 1128 | 1129 | export function MyTypeFooArgsSchema(): v.GenericSchema { 1130 | return v.object({ 1131 | a: v.nullish(v.string()), 1132 | b: v.number(), 1133 | c: v.nullish(v.boolean()), 1134 | d: v.number(), 1135 | e: v.nullish(v.string()) 1136 | }) 1137 | } 1138 | " 1139 | `) 1140 | }); 1141 | describe('with InterfaceType', () => { 1142 | it('not generate if withObjectType false', async () => { 1143 | const schema = buildSchema(/* GraphQL */ ` 1144 | interface User { 1145 | id: ID! 1146 | name: String 1147 | } 1148 | `); 1149 | const result = await plugin( 1150 | schema, 1151 | [], 1152 | { 1153 | schema: 'valibot', 1154 | withObjectType: false, 1155 | }, 1156 | {}, 1157 | ); 1158 | expect(result.content).not.toContain('export function UserSchema(): v.GenericSchema'); 1159 | }); 1160 | it('generate if withObjectType true', async () => { 1161 | const schema = buildSchema(/* GraphQL */ ` 1162 | interface Book { 1163 | title: String 1164 | } 1165 | `); 1166 | const result = await plugin( 1167 | schema, 1168 | [], 1169 | { 1170 | schema: 'valibot', 1171 | withObjectType: true, 1172 | }, 1173 | {}, 1174 | ); 1175 | expect(result.content).toMatchInlineSnapshot(` 1176 | " 1177 | 1178 | export function BookSchema(): v.GenericSchema { 1179 | return v.object({ 1180 | title: v.nullish(v.string()) 1181 | }) 1182 | } 1183 | " 1184 | `) 1185 | }); 1186 | it('generate interface type contains interface type', async () => { 1187 | const schema = buildSchema(/* GraphQL */ ` 1188 | interface Book { 1189 | author: Author 1190 | title: String 1191 | } 1192 | 1193 | interface Author { 1194 | books: [Book] 1195 | name: String 1196 | } 1197 | `); 1198 | const result = await plugin( 1199 | schema, 1200 | [], 1201 | { 1202 | schema: 'valibot', 1203 | withObjectType: true, 1204 | }, 1205 | {}, 1206 | ); 1207 | expect(result.content).toMatchInlineSnapshot(` 1208 | " 1209 | 1210 | export function BookSchema(): v.GenericSchema { 1211 | return v.object({ 1212 | author: v.nullish(AuthorSchema()), 1213 | title: v.nullish(v.string()) 1214 | }) 1215 | } 1216 | 1217 | export function AuthorSchema(): v.GenericSchema { 1218 | return v.object({ 1219 | books: v.nullish(v.array(v.nullable(BookSchema()))), 1220 | name: v.nullish(v.string()) 1221 | }) 1222 | } 1223 | " 1224 | `) 1225 | }); 1226 | it('generate object type contains interface type', async () => { 1227 | const schema = buildSchema(/* GraphQL */ ` 1228 | interface Book { 1229 | title: String! 1230 | author: Author! 1231 | } 1232 | 1233 | type Textbook implements Book { 1234 | title: String! 1235 | author: Author! 1236 | courses: [String!]! 1237 | } 1238 | 1239 | type ColoringBook implements Book { 1240 | title: String! 1241 | author: Author! 1242 | colors: [String!]! 1243 | } 1244 | 1245 | type Author { 1246 | books: [Book!] 1247 | name: String 1248 | } 1249 | `); 1250 | const result = await plugin( 1251 | schema, 1252 | [], 1253 | { 1254 | schema: 'valibot', 1255 | withObjectType: true, 1256 | }, 1257 | {}, 1258 | ); 1259 | expect(result.content).toMatchInlineSnapshot(` 1260 | " 1261 | 1262 | export function BookSchema(): v.GenericSchema { 1263 | return v.object({ 1264 | title: v.string(), 1265 | author: AuthorSchema() 1266 | }) 1267 | } 1268 | 1269 | export function TextbookSchema(): v.GenericSchema { 1270 | return v.object({ 1271 | __typename: v.optional(v.literal('Textbook')), 1272 | title: v.string(), 1273 | author: AuthorSchema(), 1274 | courses: v.array(v.string()) 1275 | }) 1276 | } 1277 | 1278 | export function ColoringBookSchema(): v.GenericSchema { 1279 | return v.object({ 1280 | __typename: v.optional(v.literal('ColoringBook')), 1281 | title: v.string(), 1282 | author: AuthorSchema(), 1283 | colors: v.array(v.string()) 1284 | }) 1285 | } 1286 | 1287 | export function AuthorSchema(): v.GenericSchema { 1288 | return v.object({ 1289 | __typename: v.optional(v.literal('Author')), 1290 | books: v.nullish(v.array(BookSchema())), 1291 | name: v.nullish(v.string()) 1292 | }) 1293 | } 1294 | " 1295 | `) 1296 | }); 1297 | }) 1298 | it.todo('properly generates custom directive values') 1299 | it.todo('exports as const instead of func') 1300 | it.todo('generate both input & type, export as const') 1301 | it.todo('issue #394') 1302 | }) 1303 | -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "incremental": false, 5 | "rootDir": "src", 6 | "types": ["node"], 7 | "declaration": false, 8 | "outDir": "dist/cjs" 9 | }, 10 | "exclude": [ 11 | "example", 12 | "tests" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "incremental": false, 5 | "target": "esnext", 6 | "rootDir": "src", 7 | "module": "esnext", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "types": ["node"], 11 | "declaration": false, 12 | "outDir": "dist/esm" 13 | }, 14 | "exclude": [ 15 | "node_modules/**", 16 | "example", 17 | "tests" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/recommended/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ES2022", 5 | "incremental": true, 6 | "declaration": true, 7 | "noEmit": false, 8 | "outDir": "dist", 9 | "baseUrl": ".", 10 | "paths": {}, 11 | "types": ["node", "vitest/globals"] 12 | }, 13 | "include": [ 14 | "src", 15 | "tests", 16 | "example" 17 | ], 18 | "exclude": [ 19 | "dist" 20 | ] 21 | } -------------------------------------------------------------------------------- /tsconfig.types.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "types": ["node"], 6 | "declarationDir": "dist/types", 7 | "emitDeclarationOnly": true, 8 | "outDir": "dist/types" 9 | }, 10 | "exclude": [ 11 | "example", 12 | "tests" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | environment: 'node', 7 | reporters: ['default'], 8 | include: ['tests/*.spec.ts'], 9 | exclude: ['node_modules', 'dist', 'example', '.idea', '.git', '.cache', '.github'], 10 | server: { 11 | deps: { 12 | fallbackCJS: true, 13 | }, 14 | }, 15 | }, 16 | }); 17 | --------------------------------------------------------------------------------