├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github ├── scripts │ └── publish-libraries.sh └── workflows │ ├── pr_on_master.yml │ ├── pre-publish.yml │ └── publish.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── README.md ├── apps ├── .gitkeep ├── codegen-e2e │ ├── .eslintrc.json │ ├── example │ │ ├── source.graphql │ │ └── test.graphql │ ├── jest.config.ts │ ├── project.json │ ├── src │ │ ├── graphql-codegen-zod │ │ │ └── output.ts │ │ ├── graphql-zod-validation │ │ │ └── zod-generated.ts │ │ └── types.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ └── tsconfig.spec.json └── example │ ├── .eslintrc.json │ ├── jest.config.ts │ ├── project.json │ ├── src │ ├── app │ │ ├── .gitkeep │ │ ├── app.controller.spec.ts │ │ ├── app.controller.ts │ │ ├── app.module.ts │ │ ├── app.service.spec.ts │ │ ├── app.service.ts │ │ └── cats │ │ │ ├── cats.controller.ts │ │ │ ├── cats.dto.ts │ │ │ └── cats.module.ts │ ├── assets │ │ └── .gitkeep │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── main.spec.ts │ └── main.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.spec.json │ └── webpack.config.js ├── codegen.yml ├── jest.config.ts ├── jest.preset.js ├── nx.json ├── package-lock.json ├── package.json ├── packages ├── .gitkeep ├── graphql-codegen-zod │ ├── .babelrc │ ├── .eslintrc.json │ ├── CHANGELOG.md │ ├── LICENSE.md │ ├── README.md │ ├── jest.config.ts │ ├── package.json │ ├── project.json │ ├── src │ │ ├── handlers │ │ │ ├── directives │ │ │ │ └── index.ts │ │ │ ├── enumsHandler.ts │ │ │ ├── fields │ │ │ │ ├── fieldHandlers.ts │ │ │ │ ├── fieldsHandler.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kindsHandler.ts │ │ │ │ └── namedTypesHandlers.ts │ │ │ ├── nodesHandler.ts │ │ │ ├── scalarsHandler.ts │ │ │ └── schemaHandler.ts │ │ ├── index.ts │ │ ├── types │ │ │ ├── graphqlTypes.ts │ │ │ └── index.ts │ │ └── utils │ │ │ ├── constants.ts │ │ │ ├── index.ts │ │ │ └── typesCheckers.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── graphql-zod-validation │ ├── .babelrc │ ├── .eslintrc.json │ ├── CHANGELOG.md │ ├── LICENSE.md │ ├── README.md │ ├── jest.config.ts │ ├── package.json │ ├── project.json │ ├── src │ │ ├── config.ts │ │ ├── directive.ts │ │ ├── graphql.ts │ │ ├── index.ts │ │ ├── myzod │ │ │ └── index.ts │ │ ├── regexp.ts │ │ ├── yup │ │ │ └── index.ts │ │ └── zod │ │ │ └── index.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── zod-mock │ ├── .babelrc │ ├── .eslintrc.json │ ├── CHANGELOG.md │ ├── LICENSE.md │ ├── README.md │ ├── jest.config.ts │ ├── package.json │ ├── project.json │ ├── src │ │ ├── index.ts │ │ └── lib │ │ │ ├── zod-mock.spec.ts │ │ │ ├── zod-mock.ts │ │ │ └── zod-mockery-map.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── zod-nestjs │ ├── .babelrc │ ├── .eslintrc.json │ ├── CHANGELOG.md │ ├── LICENSE.md │ ├── README.md │ ├── jest.config.ts │ ├── package.json │ ├── project.json │ ├── src │ │ ├── index.ts │ │ └── lib │ │ │ ├── create-zod-dto.spec.ts │ │ │ ├── create-zod-dto.ts │ │ │ ├── http-errors.ts │ │ │ ├── patch-nest-swagger.ts │ │ │ ├── types.ts │ │ │ └── zod-validation-pipe.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json └── zod-openapi │ ├── .babelrc │ ├── .eslintrc.json │ ├── CHANGELOG.md │ ├── LICENSE.md │ ├── README.md │ ├── jest.config.ts │ ├── package.json │ ├── project.json │ ├── src │ ├── index.ts │ └── lib │ │ ├── zod-extensions.spec.ts │ │ ├── zod-extensions.ts │ │ ├── zod-openapi.spec.ts │ │ └── zod-openapi.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── tools └── tsconfig.tools.json ├── tsconfig.base.json └── wallaby.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nx"], 5 | "overrides": [ 6 | { 7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 8 | "rules": { 9 | "@nx/enforce-module-boundaries": [ 10 | "error", 11 | { 12 | "enforceBuildableLibDependency": true, 13 | "allow": [], 14 | "depConstraints": [ 15 | { 16 | "sourceTag": "*", 17 | "onlyDependOnLibsWithTags": ["*"] 18 | } 19 | ] 20 | } 21 | ] 22 | } 23 | }, 24 | { 25 | "files": ["*.ts", "*.tsx"], 26 | "extends": ["plugin:@nx/typescript"], 27 | "rules": {} 28 | }, 29 | { 30 | "files": ["*.js", "*.jsx"], 31 | "extends": ["plugin:@nx/javascript"], 32 | "rules": {} 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /.github/scripts/publish-libraries.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -o errexit -o noclobber -o nounset -o pipefail 3 | 4 | # This script uses the parent version as the version to publish a library with 5 | 6 | getBuildType() { 7 | local release_type="minor" 8 | if [[ "$1" == *"feat"* ]]; then 9 | release_type="major" 10 | elif [[ "$1" == *"fix"* || "$1" == *"docs"* || "$1" == *"chore"* ]]; then 11 | release_type="patch" 12 | fi 13 | echo "$release_type" 14 | } 15 | 16 | PARENT_DIR="$PWD" 17 | ROOT_DIR="." 18 | echo "Removing Dist" 19 | rm -rf "${ROOT_DIR:?}/dist" 20 | COMMIT_MESSAGE="$(git log -1 --pretty=format:"%s")" 21 | RELEASE_TYPE=${1:-$(getBuildType "$COMMIT_MESSAGE")} 22 | DRY_RUN=${DRY_RUN:-"False"} 23 | 24 | AFFECTED=$(node node_modules/.bin/nx affected:libs --plain --base=origin/main~1) 25 | if [ "$AFFECTED" != "" ]; then 26 | cd "$PARENT_DIR" 27 | echo "Copy Environment Files" 28 | 29 | while IFS= read -r -d $' ' lib; do 30 | echo "Setting version for $lib" 31 | cd "$PARENT_DIR" 32 | cd "$ROOT_DIR/libs/${lib}" 33 | npm version "$RELEASE_TYPE" -f -m "RxJS Primitives $RELEASE_TYPE" 34 | echo "Building $lib" 35 | cd "$PARENT_DIR" 36 | npm run build "$lib" 37 | wait 38 | done <<<"$AFFECTED " # leave space on end to generate correct output 39 | 40 | cd "$PARENT_DIR" 41 | while IFS= read -r -d $' ' lib; do 42 | if [ "$DRY_RUN" == "False" ]; then 43 | echo "Publishing $lib" 44 | npm publish "$ROOT_DIR/dist/libs/${lib}" --access=public 45 | else 46 | echo "Dry Run, not publishing $lib" 47 | fi 48 | wait 49 | done <<<"$AFFECTED " # leave space on end to generate correct output 50 | else 51 | echo "No Libraries to publish" 52 | fi -------------------------------------------------------------------------------- /.github/workflows/pr_on_master.yml: -------------------------------------------------------------------------------- 1 | # File for Pull Request on main branch 2 | name: PR on main 3 | 4 | # When a PR is opened to main 5 | on: 6 | pull_request: 7 | branches: 8 | - main 9 | types: [opened, reopened, synchronize] 10 | 11 | env: 12 | BEFORE_SHA: ${{ github.event.before }} 13 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 14 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 15 | 16 | jobs: 17 | build: 18 | # Setup OS and Node Version 19 | runs-on: ubuntu-latest 20 | strategy: 21 | matrix: 22 | # Latest nodes only 23 | node-version: [18] 24 | 25 | # Define Steps 26 | steps: 27 | # Checkout code 28 | - name: Checkout 29 | uses: actions/checkout@v3 30 | with: 31 | fetch-depth: 0 32 | 33 | - name: Use Node.js ${{ matrix.node-version }} 34 | uses: actions/setup-node@v3 35 | with: 36 | node-version: ${{ matrix.node-version }} 37 | 38 | # Make sure we have all branches 39 | - name: Fetch other branches 40 | run: git fetch --no-tags --prune --depth=5 origin main 41 | 42 | - name: Install environment 43 | run: npm install 44 | 45 | - name: Run lint 46 | run: npm run affected:lint -- --base="origin/main" 47 | 48 | - name: Tests coverage 49 | run: npm run affected:test -- --base="origin/main" --codeCoverage 50 | -------------------------------------------------------------------------------- /.github/workflows/pre-publish.yml: -------------------------------------------------------------------------------- 1 | name: Test-Release Build 2 | 3 | on: 4 | - workflow_dispatch 5 | 6 | jobs: 7 | test-deploy: 8 | # Setup OS and Node Version 9 | runs-on: ubuntu-latest 10 | 11 | env: 12 | GITHUB_TOKEN: ${{ secrets.DEPLOY_FLOW_TOKEN }} 13 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 14 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 15 | 16 | steps: 17 | # Checkout code 18 | - name: Checkout Code 19 | uses: actions/checkout@v3 20 | with: 21 | fetch-depth: 0 22 | token: ${{ secrets.DEPLOY_FLOW_TOKEN }} 23 | # Install deps 24 | - name: Use Node.js 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: '18' 28 | 29 | - name: Setup Git 30 | run: | 31 | git config user.name "GitHub Bot" 32 | git config user.email "Brian-McBride@users.noreply.github.com" 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.DEPLOY_FLOW_TOKEN }} 35 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 36 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 37 | 38 | - name: Configure npm 39 | run: | 40 | echo '//registry.npmjs.org/:_authToken=${NPM_TOKEN}' > .npmrc 41 | env: 42 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 43 | 44 | - name: Install environment 45 | run: npm install --legacy-peer-deps 46 | 47 | - name: Test 48 | run: npm run affected:test 49 | 50 | - name: Build 51 | run: npm run affected:build 52 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | - workflow_dispatch 5 | 6 | jobs: 7 | deploy: 8 | # Setup OS and Node Version 9 | runs-on: ubuntu-latest 10 | 11 | env: 12 | GITHUB_TOKEN: ${{ secrets.DEPLOY_FLOW_TOKEN }} 13 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 14 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 15 | 16 | steps: 17 | # Checkout code 18 | - name: Checkout Code 19 | uses: actions/checkout@v3 20 | with: 21 | fetch-depth: 0 22 | token: ${{ secrets.DEPLOY_FLOW_TOKEN }} 23 | # Install deps 24 | - name: Use Node.js 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: '18' 28 | 29 | - name: Setup Git 30 | run: | 31 | git config user.name "GitHub Bot" 32 | git config user.email "Brian-McBride@users.noreply.github.com" 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.DEPLOY_FLOW_TOKEN }} 35 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 36 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 37 | 38 | - name: Configure npm 39 | run: | 40 | echo '//registry.npmjs.org/:_authToken=${NPM_TOKEN}' > .npmrc 41 | env: 42 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 43 | 44 | - name: Install environment 45 | run: npm install --legacy-peer-deps 46 | 47 | - name: Test 48 | run: npm run affected:test 49 | 50 | - name: Version 51 | shell: bash 52 | run: npm run affected:version 53 | env: 54 | GITHUB_TOKEN: ${{ secrets.DEPLOY_FLOW_TOKEN }} 55 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 56 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 57 | 58 | ###### DEPRECIATED ###### 59 | # - name: Build 60 | # run: npm run affected:build 61 | 62 | # - name: publish 63 | # run: npm run affected:publish 64 | # env: 65 | # NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 66 | # NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 67 | 68 | # - name: git-update 69 | # shell: bash 70 | # run: npm run affected:github 71 | # env: 72 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 73 | # NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 74 | # NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 75 | ######################### 76 | 77 | - name: Return npm 78 | run: | 79 | echo 'legacy-peer-deps=true' > .npmrc 80 | 81 | - name: Tag last-release 82 | shell: bash 83 | run: | 84 | git tag -f last-release 85 | git push origin last-release --force 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | /dist 4 | /coverage 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "nrwl.angular-console", 4 | "esbenp.prettier-vscode", 5 | "firsttris.vscode-jest-runner", 6 | "dbaeumer.vscode-eslint" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "anatidae", 4 | "anatine", 5 | "codegen", 6 | "deepmerge", 7 | "esbuild", 8 | "jscutlery", 9 | "mersenne", 10 | "metatype", 11 | "myzod", 12 | "Nonpositive", 13 | "npmrc", 14 | "openapi", 15 | "randexp", 16 | "Unprocessable" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # @anatine/zod-plugins 3 | 4 | Various modules to help leverage zod in all sort so places 5 | 6 | ## Packages within this repository 7 | 8 | - ### [@anatine/zod-openapi](./packages/zod-openapi/README.md) 9 | 10 | - Converts a Zod schema into an OpenAPI 3.0 `SchemaObject` 11 | - Leverages [openapi3-ts](https://www.npmjs.com/package/openapi3-ts) for typings and builders 12 | 13 | - ### [@anatine/zod-mock](./packages/zod-mock/README.md) 14 | 15 | - Generates a mock object for testing. 16 | - Fake data generated from the peer dependency [faker.js](https://fakerjs.dev/) 17 | 18 | - ### [@anatine/zod-nestjs](./packages/zod-nestjs/README.md) 19 | 20 | - Helper tooling to use Zod in [NestJS](https://nestjs.com/). 21 | - Patch for [@nestjs/swagger](https://docs.nestjs.com/openapi/introduction) to use `@anatine/zod-openapi` to display schemas. 22 | 23 | - ### [@anatine/graphql-zod-validation](./packages/graphql-zod-validation/README.md) 24 | 25 | - Used with [GraphQL code generator](https://github.com/dotansimha/graphql-code-generator) plugin to generate form validation schema from your GraphQL schema. 26 | 27 | - ### [@anatine/graphql-codegen-zod](./packages/graphql-codegen-zod/README.md) 28 | 29 | - Used with [GraphQL code generator](https://github.com/dotansimha/graphql-code-generator) plugin to generate form validation schema from your GraphQL schema. 30 | - Alternative codebase to [@anatine/graphql-zod-validation](./packages/graphql-zod-validation/README.md) 31 | 32 | ---- 33 | 34 | This is a monorepo project utilizing the tooling [Nx](https://nx.dev). 35 | -------------------------------------------------------------------------------- /apps/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anatine/zod-plugins/819fbec3eb95ee17a6ad8076341213417e736736/apps/.gitkeep -------------------------------------------------------------------------------- /apps/codegen-e2e/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /apps/codegen-e2e/example/source.graphql: -------------------------------------------------------------------------------- 1 | directive @validation( 2 | pattern: String 3 | min: Int 4 | max: Int 5 | requiredMessage: String 6 | typeOf: String 7 | ) on INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION 8 | 9 | enum TestEnum { 10 | ENUM1 11 | ENUM2 12 | } 13 | 14 | type TestExample { 15 | id: ID! 16 | name: String! 17 | email: String 18 | } 19 | 20 | input TestInput { 21 | string: String 22 | stringRequired: String! 23 | enum: TestEnum 24 | enumRequired: TestEnum! 25 | scalar: EmailAddress 26 | scalarRequired: EmailAddress! 27 | enumArray: [TestEnum] 28 | enumArrayRequired: [TestEnum]! 29 | emailArray: [EmailAddress] 30 | emailArrayRequired: [EmailAddress]! 31 | emailRequiredArray: [EmailAddress!] 32 | emailRequiredArrayRequired: [EmailAddress!]! 33 | stringArray: [String] 34 | stringArrayRequired: [String]! 35 | stringRequiredArray: [String!] 36 | stringRequiredArrayRequired: [String!]! 37 | } 38 | 39 | input RegisterAddressInput { 40 | # postalCode: TestInput! 41 | state: [String]! 42 | city: String! 43 | someNumber: Int @validation(min: 10, max: 20) 44 | someNumberFloat: Float @validation(min: 10.50, max: 20.50) 45 | someBoolean: Boolean 46 | ipAddress: IPAddress 47 | line2: String @validation(min: 10, max: 20) 48 | } 49 | 50 | scalar EmailAddress 51 | scalar IPAddress 52 | -------------------------------------------------------------------------------- /apps/codegen-e2e/example/test.graphql: -------------------------------------------------------------------------------- 1 | enum PageType { 2 | LP 3 | SERVICE 4 | RESTRICTED 5 | BASIC_AUTH 6 | } 7 | 8 | input PageInput { 9 | id: ID! 10 | title: String! 11 | description: String 12 | show: Boolean! 13 | width: Int! 14 | height: Float! 15 | layout: LayoutInput! 16 | tags: [String] 17 | attributes: [AttributeInput!] 18 | pageType: PageType! 19 | date: Date 20 | postIDs: [ID!] 21 | } 22 | 23 | input AttributeInput { 24 | key: String 25 | val: String 26 | } 27 | 28 | input LayoutInput { 29 | dropdown: DropDownComponentInput 30 | } 31 | 32 | input DropDownComponentInput { 33 | getEvent: EventInput! 34 | dropdownComponent: ComponentInput 35 | } 36 | 37 | enum ButtonComponentType { 38 | BUTTON 39 | SUBMIT 40 | } 41 | 42 | input ComponentInput { 43 | type: ButtonComponentType! 44 | name: String! 45 | event: EventInput 46 | child: ComponentInput 47 | childrens: [ComponentInput] 48 | } 49 | 50 | input EventInput { 51 | arguments: [EventArgumentInput!]! 52 | options: [EventOptionType!] 53 | } 54 | 55 | enum EventOptionType { 56 | RETRY 57 | RELOAD 58 | } 59 | 60 | input EventArgumentInput { 61 | name: String! @constraint(minLength: 5) 62 | value: String! @constraint(startsWith: "foo") 63 | } 64 | 65 | input HTTPInput { 66 | method: HTTPMethod 67 | url: URL! 68 | } 69 | 70 | enum HTTPMethod { 71 | GET 72 | POST 73 | } 74 | 75 | scalar Date 76 | scalar URL 77 | 78 | # https://github.com/confuser/graphql-constraint-directive 79 | directive @constraint( 80 | # String constraints 81 | minLength: Int 82 | maxLength: Int 83 | startsWith: String 84 | endsWith: String 85 | contains: String 86 | notContains: String 87 | pattern: String 88 | format: String 89 | # Number constraints 90 | min: Float 91 | max: Float 92 | exclusiveMin: Float 93 | exclusiveMax: Float 94 | multipleOf: Float 95 | uniqueTypeName: String 96 | ) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION 97 | -------------------------------------------------------------------------------- /apps/codegen-e2e/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'codegen-e2e', 4 | preset: '../../jest.preset.js', 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | }, 9 | }, 10 | testEnvironment: 'node', 11 | transform: { 12 | '^.+\\.[tj]s$': 'ts-jest', 13 | }, 14 | moduleFileExtensions: ['ts', 'js', 'html'], 15 | coverageDirectory: '../../coverage/apps/codegen-e2e', 16 | }; 17 | -------------------------------------------------------------------------------- /apps/codegen-e2e/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codegen-e2e", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "apps/codegen-e2e/src", 5 | "projectType": "application", 6 | "implicitDependencies": ["graphql-codegen-zod", "graphql-zod-validation"], 7 | "targets": { 8 | "test": { 9 | "executor": "nx:run-commands", 10 | "options": { 11 | "color": true, 12 | "parallel": false, 13 | "commands": ["echo \"Code test not enabled...\""] 14 | } 15 | } 16 | }, 17 | "tags": ["test"] 18 | } 19 | -------------------------------------------------------------------------------- /apps/codegen-e2e/src/graphql-codegen-zod/output.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | 4 | 5 | export const DateSchema = z.string(); 6 | export const EmailAddressSchema: z.ZodSchema = z.string().email(); 7 | export const IPAddressSchema = z.string(); 8 | export const URLSchema = z.string(); 9 | 10 | export const ButtonComponentTypeSchema = z.enum(["BUTTON","SUBMIT"]); 11 | export const EventOptionTypeSchema = z.enum(["RELOAD","RETRY"]); 12 | export const HTTPMethodSchema = z.enum(["GET","POST"]); 13 | export const PageTypeSchema = z.enum(["BASIC_AUTH","LP","RESTRICTED","SERVICE"]); 14 | export const TestEnumSchema = z.enum(["ENUM1","ENUM2"]); 15 | 16 | export const AttributeInputSchema = z.lazy(() => z.object({ 17 | key: z.string().nullish(), 18 | val: z.string().nullish() 19 | })); 20 | 21 | 22 | export const ComponentInputSchema = z.lazy(() => z.object({ 23 | child: ComponentInputSchema.nullish(), 24 | childrens: z.array(ComponentInputSchema.nullish()).nullish(), 25 | event: EventInputSchema.nullish(), 26 | name: z.string(), 27 | type: ButtonComponentTypeSchema 28 | })); 29 | 30 | 31 | export const DropDownComponentInputSchema = z.lazy(() => z.object({ 32 | dropdownComponent: ComponentInputSchema.nullish(), 33 | getEvent: EventInputSchema 34 | })); 35 | 36 | 37 | export const EventArgumentInputSchema = z.lazy(() => z.object({ 38 | name: z.string(), 39 | value: z.string() 40 | })); 41 | 42 | 43 | export const EventInputSchema = z.lazy(() => z.object({ 44 | arguments: z.array(EventArgumentInputSchema), 45 | options: z.array(EventOptionTypeSchema).nullish() 46 | })); 47 | 48 | 49 | export const HTTPInputSchema = z.lazy(() => z.object({ 50 | method: HTTPMethodSchema.nullish(), 51 | url: URLSchema 52 | })); 53 | 54 | 55 | export const LayoutInputSchema = z.lazy(() => z.object({ 56 | dropdown: DropDownComponentInputSchema.nullish() 57 | })); 58 | 59 | 60 | export const PageInputSchema = z.lazy(() => z.object({ 61 | attributes: z.array(AttributeInputSchema).nullish(), 62 | date: DateSchema.nullish(), 63 | description: z.string().nullish(), 64 | height: z.number(), 65 | id: z.string(), 66 | layout: LayoutInputSchema, 67 | pageType: PageTypeSchema, 68 | postIDs: z.array(z.string()).nullish(), 69 | show: z.boolean(), 70 | tags: z.array(z.string().nullish()).nullish(), 71 | title: z.string(), 72 | width: z.number() 73 | })); 74 | 75 | 76 | export const RegisterAddressInputSchema = z.lazy(() => z.object({ 77 | city: z.string(), 78 | ipAddress: IPAddressSchema.nullish(), 79 | line2: z.string().min(10).max(20).nullish(), 80 | someBoolean: z.boolean().nullish(), 81 | someNumber: z.number().min(10).max(20).nullish(), 82 | someNumberFloat: z.number().min(10.5).max(20.5).nullish(), 83 | state: z.array(z.string().nullish()) 84 | })); 85 | 86 | 87 | export const TestExampleSchema = z.lazy(() => z.object({ 88 | email: z.string().nullish(), 89 | id: z.string(), 90 | name: z.string() 91 | })); 92 | 93 | 94 | export const TestInputSchema = z.lazy(() => z.object({ 95 | emailArray: z.array(EmailAddressSchema.nullish()).nullish(), 96 | emailArrayRequired: z.array(EmailAddressSchema.nullish()), 97 | emailRequiredArray: z.array(EmailAddressSchema).nullish(), 98 | emailRequiredArrayRequired: z.array(EmailAddressSchema), 99 | enum: TestEnumSchema.nullish(), 100 | enumArray: z.array(TestEnumSchema.nullish()).nullish(), 101 | enumArrayRequired: z.array(TestEnumSchema.nullish()), 102 | enumRequired: TestEnumSchema, 103 | scalar: EmailAddressSchema.nullish(), 104 | scalarRequired: EmailAddressSchema, 105 | string: z.string().nullish(), 106 | stringArray: z.array(z.string().nullish()).nullish(), 107 | stringArrayRequired: z.array(z.string().nullish()), 108 | stringRequired: z.string(), 109 | stringRequiredArray: z.array(z.string()).nullish(), 110 | stringRequiredArrayRequired: z.array(z.string()) 111 | })) -------------------------------------------------------------------------------- /apps/codegen-e2e/src/graphql-zod-validation/zod-generated.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import { AttributeInput, ButtonComponentType, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, HttpInput, HttpMethod, LayoutInput, PageInput, PageType, RegisterAddressInput, TestEnum, TestExample, TestInput } 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 function AttributeInputSchema(): z.ZodObject> { 15 | return z.object({ 16 | key: z.string().nullish(), 17 | val: z.string().nullish() 18 | }) 19 | } 20 | 21 | export const ButtonComponentTypeSchema = z.nativeEnum(ButtonComponentType); 22 | 23 | export function ComponentInputSchema(): z.ZodObject> { 24 | return z.object({ 25 | child: z.lazy(() => ComponentInputSchema().nullish()), 26 | childrens: z.array(z.lazy(() => ComponentInputSchema().nullable())).nullish(), 27 | event: z.lazy(() => EventInputSchema().nullish()), 28 | name: z.string(), 29 | type: ButtonComponentTypeSchema 30 | }) 31 | } 32 | 33 | export function DropDownComponentInputSchema(): z.ZodObject> { 34 | return z.object({ 35 | dropdownComponent: z.lazy(() => ComponentInputSchema().nullish()), 36 | getEvent: z.lazy(() => EventInputSchema()) 37 | }) 38 | } 39 | 40 | export function EventArgumentInputSchema(): z.ZodObject> { 41 | return z.object({ 42 | name: z.string(), 43 | value: z.string() 44 | }) 45 | } 46 | 47 | export function EventInputSchema(): z.ZodObject> { 48 | return z.object({ 49 | arguments: z.array(z.lazy(() => EventArgumentInputSchema())), 50 | options: z.array(EventOptionTypeSchema).nullish() 51 | }) 52 | } 53 | 54 | export const EventOptionTypeSchema = z.nativeEnum(EventOptionType); 55 | 56 | export function HttpInputSchema(): z.ZodObject> { 57 | return z.object({ 58 | method: HttpMethodSchema.nullish(), 59 | url: z.string() 60 | }) 61 | } 62 | 63 | export const HttpMethodSchema = z.nativeEnum(HttpMethod); 64 | 65 | export function LayoutInputSchema(): z.ZodObject> { 66 | return z.object({ 67 | dropdown: z.lazy(() => DropDownComponentInputSchema().nullish()) 68 | }) 69 | } 70 | 71 | export function PageInputSchema(): z.ZodObject> { 72 | return z.object({ 73 | attributes: z.array(z.lazy(() => AttributeInputSchema())).nullish(), 74 | date: z.string().nullish(), 75 | description: z.string().nullish(), 76 | height: z.number(), 77 | id: z.string(), 78 | layout: z.lazy(() => LayoutInputSchema()), 79 | pageType: PageTypeSchema, 80 | postIDs: z.array(z.string()).nullish(), 81 | show: z.boolean(), 82 | tags: z.array(z.string().nullable()).nullish(), 83 | title: z.string(), 84 | width: z.number() 85 | }) 86 | } 87 | 88 | export const PageTypeSchema = z.nativeEnum(PageType); 89 | 90 | export function RegisterAddressInputSchema(): z.ZodObject> { 91 | return z.object({ 92 | city: z.string(), 93 | ipAddress: z.string().nullish(), 94 | line2: z.string().nullish(), 95 | someBoolean: z.boolean().nullish(), 96 | someNumber: z.number().nullish(), 97 | someNumberFloat: z.number().nullish(), 98 | state: z.array(z.string().nullable()) 99 | }) 100 | } 101 | 102 | export const TestEnumSchema = z.nativeEnum(TestEnum); 103 | 104 | export function TestExampleSchema(): z.ZodObject> { 105 | return z.object({ 106 | __typename: z.literal('TestExample').optional(), 107 | email: z.string().nullish(), 108 | id: z.string(), 109 | name: z.string() 110 | }) 111 | } 112 | 113 | export function TestInputSchema(): z.ZodObject> { 114 | return z.object({ 115 | emailArray: z.array(z.string().email().nullable()).nullish(), 116 | emailArrayRequired: z.array(z.string().email().nullable()), 117 | emailRequiredArray: z.array(z.string().email()).nullish(), 118 | emailRequiredArrayRequired: z.array(z.string().email()), 119 | enum: TestEnumSchema.nullish(), 120 | enumArray: z.array(TestEnumSchema.nullable()).nullish(), 121 | enumArrayRequired: z.array(TestEnumSchema.nullable()), 122 | enumRequired: TestEnumSchema, 123 | scalar: z.string().email().nullish(), 124 | scalarRequired: z.string().email(), 125 | string: z.string().nullish(), 126 | stringArray: z.array(z.string().nullable()).nullish(), 127 | stringArrayRequired: z.array(z.string().nullable()), 128 | stringRequired: z.string(), 129 | stringRequiredArray: z.array(z.string()).nullish(), 130 | stringRequiredArrayRequired: z.array(z.string()) 131 | }) 132 | } 133 | -------------------------------------------------------------------------------- /apps/codegen-e2e/src/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 | /** All built-in and custom scalars, mapped to their actual values */ 7 | export type Scalars = { 8 | ID: string; 9 | String: string; 10 | Boolean: boolean; 11 | Int: number; 12 | Float: number; 13 | Date: any; 14 | EmailAddress: any; 15 | IPAddress: any; 16 | URL: any; 17 | }; 18 | 19 | export type AttributeInput = { 20 | key?: InputMaybe; 21 | val?: InputMaybe; 22 | }; 23 | 24 | export enum ButtonComponentType { 25 | Button = 'BUTTON', 26 | Submit = 'SUBMIT' 27 | } 28 | 29 | export type ComponentInput = { 30 | child?: InputMaybe; 31 | childrens?: InputMaybe>>; 32 | event?: InputMaybe; 33 | name: Scalars['String']; 34 | type: ButtonComponentType; 35 | }; 36 | 37 | export type DropDownComponentInput = { 38 | dropdownComponent?: InputMaybe; 39 | getEvent: EventInput; 40 | }; 41 | 42 | export type EventArgumentInput = { 43 | name: Scalars['String']; 44 | value: Scalars['String']; 45 | }; 46 | 47 | export type EventInput = { 48 | arguments: Array; 49 | options?: InputMaybe>; 50 | }; 51 | 52 | export enum EventOptionType { 53 | Reload = 'RELOAD', 54 | Retry = 'RETRY' 55 | } 56 | 57 | export type HttpInput = { 58 | method?: InputMaybe; 59 | url: Scalars['URL']; 60 | }; 61 | 62 | export enum HttpMethod { 63 | Get = 'GET', 64 | Post = 'POST' 65 | } 66 | 67 | export type LayoutInput = { 68 | dropdown?: InputMaybe; 69 | }; 70 | 71 | export type PageInput = { 72 | attributes?: InputMaybe>; 73 | date?: InputMaybe; 74 | description?: InputMaybe; 75 | height: Scalars['Float']; 76 | id: Scalars['ID']; 77 | layout: LayoutInput; 78 | pageType: PageType; 79 | postIDs?: InputMaybe>; 80 | show: Scalars['Boolean']; 81 | tags?: InputMaybe>>; 82 | title: Scalars['String']; 83 | width: Scalars['Int']; 84 | }; 85 | 86 | export enum PageType { 87 | BasicAuth = 'BASIC_AUTH', 88 | Lp = 'LP', 89 | Restricted = 'RESTRICTED', 90 | Service = 'SERVICE' 91 | } 92 | 93 | export type RegisterAddressInput = { 94 | city: Scalars['String']; 95 | ipAddress?: InputMaybe; 96 | line2?: InputMaybe; 97 | someBoolean?: InputMaybe; 98 | someNumber?: InputMaybe; 99 | someNumberFloat?: InputMaybe; 100 | state: Array>; 101 | }; 102 | 103 | export enum TestEnum { 104 | Enum1 = 'ENUM1', 105 | Enum2 = 'ENUM2' 106 | } 107 | 108 | export type TestExample = { 109 | __typename?: 'TestExample'; 110 | email?: Maybe; 111 | id: Scalars['ID']; 112 | name: Scalars['String']; 113 | }; 114 | 115 | export type TestInput = { 116 | emailArray?: InputMaybe>>; 117 | emailArrayRequired: Array>; 118 | emailRequiredArray?: InputMaybe>; 119 | emailRequiredArrayRequired: Array; 120 | enum?: InputMaybe; 121 | enumArray?: InputMaybe>>; 122 | enumArrayRequired: Array>; 123 | enumRequired: TestEnum; 124 | scalar?: InputMaybe; 125 | scalarRequired: Scalars['EmailAddress']; 126 | string?: InputMaybe; 127 | stringArray?: InputMaybe>>; 128 | stringArrayRequired: Array>; 129 | stringRequired: Scalars['String']; 130 | stringRequiredArray?: InputMaybe>; 131 | stringRequiredArrayRequired: Array; 132 | }; 133 | -------------------------------------------------------------------------------- /apps/codegen-e2e/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["node"] 7 | }, 8 | "exclude": ["jest.config.ts", "**/*.spec.ts", "**/*.test.ts"], 9 | "include": ["**/*.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /apps/codegen-e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.app.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /apps/codegen-e2e/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /apps/example/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /apps/example/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'example', 4 | preset: '../../jest.preset.js', 5 | testEnvironment: 'node', 6 | transform: { 7 | '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], 8 | }, 9 | moduleFileExtensions: ['ts', 'js', 'html'], 10 | coverageDirectory: '../../coverage/apps/example', 11 | }; 12 | -------------------------------------------------------------------------------- /apps/example/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "apps/example/src", 5 | "projectType": "application", 6 | "implicitDependencies": ["zod-openapi", "zod-nestjs"], 7 | "targets": { 8 | "build": { 9 | "executor": "@nx/webpack:webpack", 10 | "outputs": ["{options.outputPath}"], 11 | "options": { 12 | "target": "node", 13 | "compiler": "tsc", 14 | "outputPath": "dist/apps/example", 15 | "main": "apps/example/src/main.ts", 16 | "tsConfig": "apps/example/tsconfig.app.json", 17 | "assets": ["apps/example/src/assets"], 18 | "isolatedConfig": true, 19 | "webpackConfig": "apps/example/webpack.config.js" 20 | }, 21 | "configurations": { 22 | "production": { 23 | "optimization": true, 24 | "extractLicenses": true, 25 | "inspect": false, 26 | "fileReplacements": [ 27 | { 28 | "replace": "apps/example/src/environments/environment.ts", 29 | "with": "apps/example/src/environments/environment.prod.ts" 30 | } 31 | ] 32 | } 33 | } 34 | }, 35 | "serve": { 36 | "executor": "@nx/js:node", 37 | "options": { 38 | "buildTarget": "example:build" 39 | } 40 | }, 41 | "lint": { 42 | "executor": "@nx/linter:eslint", 43 | "options": { 44 | "lintFilePatterns": ["apps/example/**/*.ts"] 45 | }, 46 | "outputs": ["{options.outputFile}"] 47 | }, 48 | "test": { 49 | "executor": "@nx/jest:jest", 50 | "outputs": ["{workspaceRoot}/coverage/apps/example"], 51 | "options": { 52 | "jestConfig": "apps/example/jest.config.ts", 53 | "passWithNoTests": true 54 | } 55 | } 56 | }, 57 | "tags": ["test"] 58 | } 59 | -------------------------------------------------------------------------------- /apps/example/src/app/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anatine/zod-plugins/819fbec3eb95ee17a6ad8076341213417e736736/apps/example/src/app/.gitkeep -------------------------------------------------------------------------------- /apps/example/src/app/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | 3 | import { AppController } from './app.controller'; 4 | import { AppService } from './app.service'; 5 | 6 | describe('AppController', () => { 7 | let app: TestingModule; 8 | 9 | beforeAll(async () => { 10 | app = await Test.createTestingModule({ 11 | controllers: [AppController], 12 | providers: [AppService], 13 | }).compile(); 14 | }); 15 | 16 | describe('getData', () => { 17 | it('should return "Welcome to example!"', () => { 18 | const appController = app.get(AppController); 19 | expect(appController.getData()).toEqual({ 20 | message: 'Welcome to example!', 21 | }); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /apps/example/src/app/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | 3 | import { AppService } from './app.service'; 4 | 5 | @Controller() 6 | export class AppController { 7 | constructor(private readonly appService: AppService) {} 8 | 9 | @Get() 10 | getData() { 11 | return this.appService.getData(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /apps/example/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { AppController } from './app.controller'; 4 | import { AppService } from './app.service'; 5 | import { CatsModule } from './cats/cats.module'; 6 | 7 | @Module({ 8 | imports: [CatsModule], 9 | controllers: [AppController], 10 | providers: [AppService], 11 | }) 12 | export class AppModule {} 13 | -------------------------------------------------------------------------------- /apps/example/src/app/app.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppService', () => { 6 | let service: AppService; 7 | 8 | beforeAll(async () => { 9 | const app = await Test.createTestingModule({ 10 | providers: [AppService], 11 | }).compile(); 12 | 13 | service = app.get(AppService); 14 | }); 15 | 16 | describe('getData', () => { 17 | it('should return "Welcome to example!"', () => { 18 | expect(service.getData()).toEqual({ message: 'Welcome to example!' }); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /apps/example/src/app/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getData(): { message: string } { 6 | return { message: 'Welcome to example!' }; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/example/src/app/cats/cats.controller.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { ZodValidationPipe } from '@anatine/zod-nestjs'; 3 | import { 4 | Body, 5 | Controller, 6 | Get, 7 | Param, 8 | Patch, 9 | Post, 10 | UsePipes, 11 | } from '@nestjs/common'; 12 | import { ApiCreatedResponse } from '@nestjs/swagger'; 13 | import { 14 | CatDto, 15 | CreateCatResponseDto, 16 | GetCatsDto, 17 | GetCatsParamsDto, 18 | UpdateCatDto, 19 | UpdateCatResponseDto, 20 | } from './cats.dto'; 21 | 22 | @Controller('cats') 23 | @UsePipes(ZodValidationPipe) 24 | export class CatsController { 25 | // Get all cats 26 | @Get() 27 | @ApiCreatedResponse({ 28 | type: GetCatsDto, 29 | }) 30 | async findAll(): Promise { 31 | return { cats: ['Lizzie', 'Spike'] }; 32 | } 33 | 34 | // Get single cat 35 | @Get(':id') 36 | @ApiCreatedResponse({ 37 | type: CatDto, 38 | }) 39 | async findOne(@Param() { id }: GetCatsParamsDto): Promise { 40 | return { 41 | name: `Cat-${id}`, 42 | age: 8, 43 | breed: 'Unknown', 44 | }; 45 | } 46 | 47 | @Post() 48 | @ApiCreatedResponse({ 49 | description: 'The record has been successfully created.', 50 | type: CreateCatResponseDto, 51 | }) 52 | async create(@Body() createCatDto: CatDto): Promise { 53 | return { 54 | success: true, 55 | message: 'Cat created', 56 | name: createCatDto.name, 57 | }; 58 | } 59 | 60 | @Patch() 61 | async update( 62 | @Body() updateCatDto: UpdateCatDto 63 | ): Promise { 64 | return { 65 | success: true, 66 | message: `Cat's age of ${updateCatDto.age} updated`, 67 | }; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /apps/example/src/app/cats/cats.dto.ts: -------------------------------------------------------------------------------- 1 | import { createZodDto } from '@anatine/zod-nestjs'; 2 | import { extendApi } from '@anatine/zod-openapi'; 3 | import { z } from 'zod'; 4 | 5 | export const CatZ = extendApi( 6 | z.object({ 7 | name: z.string(), 8 | age: z.number(), 9 | breed: z.string(), 10 | }), 11 | { 12 | title: 'Cat', 13 | description: 'A cat', 14 | } 15 | ); 16 | export class CatDto extends createZodDto(CatZ) {} 17 | export class UpdateCatDto extends createZodDto(CatZ.omit({ name: true })) {} 18 | 19 | export const GetCatsZ = extendApi( 20 | z.object({ 21 | cats: extendApi(z.array(z.string()), { description: 'List of cats' }), 22 | }), 23 | { title: 'Get Cat Response' } 24 | ); 25 | export class GetCatsDto extends createZodDto(GetCatsZ) {} 26 | 27 | export const CreateCatResponseZ = z.object({ 28 | success: z.boolean(), 29 | message: z.string(), 30 | name: z.string(), 31 | }); 32 | 33 | export class CreateCatResponseDto extends createZodDto(CreateCatResponseZ) {} 34 | export class UpdateCatResponseDto extends createZodDto( 35 | CreateCatResponseZ.omit({ name: true }) 36 | ) {} 37 | 38 | export const GetCatsParamsZ = extendApi( 39 | z.object({ 40 | id: z.string(), 41 | }), 42 | { 43 | examples: [{ id: 'mouse-terminator-2000' }], 44 | } 45 | ); 46 | 47 | export class GetCatsParamsDto extends createZodDto(GetCatsParamsZ) {} 48 | -------------------------------------------------------------------------------- /apps/example/src/app/cats/cats.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { CatsController } from './cats.controller'; 3 | 4 | @Module({ 5 | imports: [], 6 | providers: [], 7 | controllers: [CatsController], 8 | }) 9 | export class CatsModule {} 10 | -------------------------------------------------------------------------------- /apps/example/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anatine/zod-plugins/819fbec3eb95ee17a6ad8076341213417e736736/apps/example/src/assets/.gitkeep -------------------------------------------------------------------------------- /apps/example/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | }; 4 | -------------------------------------------------------------------------------- /apps/example/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false, 3 | }; 4 | -------------------------------------------------------------------------------- /apps/example/src/main.spec.ts: -------------------------------------------------------------------------------- 1 | import * as request from 'supertest'; 2 | import { Test } from '@nestjs/testing'; 3 | import { INestApplication } from '@nestjs/common'; 4 | import { AppModule } from './app/app.module'; 5 | import { patchNestjsSwagger } from '@anatine/zod-nestjs'; 6 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 7 | 8 | describe('Cats', () => { 9 | let app: INestApplication; 10 | 11 | beforeAll(async () => { 12 | const moduleRef = await Test.createTestingModule({ 13 | imports: [AppModule], 14 | }).compile(); 15 | 16 | app = moduleRef.createNestApplication(); 17 | 18 | const config = new DocumentBuilder() 19 | .setTitle('Cats example') 20 | .setDescription('The cats API description') 21 | .setVersion('1.0') 22 | .addTag('cats') 23 | .build(); 24 | 25 | patchNestjsSwagger(); 26 | const document = SwaggerModule.createDocument(app, config); 27 | SwaggerModule.setup('api', app, document); 28 | 29 | await app.init(); 30 | }); 31 | 32 | it(`/GET cats`, () => { 33 | // return request(app.getHttpServer()).get('/cats').expect(200).expect({ 34 | // data: 'This action returns all cats', 35 | // }); 36 | return request(app.getHttpServer()) 37 | .get('/cats') 38 | .set('Accept', 'application/json') 39 | .expect(200) 40 | .expect({ cats: ['Lizzie', 'Spike'] }); 41 | }); 42 | 43 | it(`/POST cats`, () => { 44 | return request(app.getHttpServer()) 45 | .post('/cats') 46 | .set('Accept', 'application/json') 47 | .send({ name: 'Spike', age: 3, breed: 'Persian' }) 48 | .expect(201) 49 | .expect({ success: true, message: 'Cat created', name: 'Spike' }); 50 | }); 51 | 52 | // it('Lets peek', async () => { 53 | // const result = await request(app.getHttpServer()) 54 | // .get('/api-json') 55 | // .set('Accept', 'application/json'); 56 | 57 | // const { body } = result; 58 | 59 | // console.log(inspect(body, false, 10, true)); 60 | // }); 61 | 62 | it(`Swagger Test`, async () => { 63 | return request(app.getHttpServer()) 64 | .get('/api-json') 65 | .set('Accept', 'application/json') 66 | .expect(200) 67 | .expect({ 68 | openapi: '3.0.0', 69 | paths: { 70 | '/': { 71 | get: { 72 | operationId: 'AppController_getData', 73 | parameters: [], 74 | responses: { '200': { description: '' } }, 75 | tags: ['App'] 76 | }, 77 | }, 78 | '/cats': { 79 | get: { 80 | operationId: 'CatsController_findAll', 81 | parameters: [], 82 | responses: { 83 | '201': { 84 | description: '', 85 | content: { 86 | 'application/json': { 87 | schema: { $ref: '#/components/schemas/GetCatsDto' }, 88 | }, 89 | }, 90 | }, 91 | }, 92 | tags: [`Cats`], 93 | }, 94 | post: { 95 | operationId: 'CatsController_create', 96 | parameters: [], 97 | requestBody: { 98 | required: true, 99 | content: { 100 | 'application/json': { 101 | schema: { $ref: '#/components/schemas/CatDto' }, 102 | }, 103 | }, 104 | }, 105 | responses: { 106 | '201': { 107 | description: 'The record has been successfully created.', 108 | content: { 109 | 'application/json': { 110 | schema: { 111 | $ref: '#/components/schemas/CreateCatResponseDto', 112 | }, 113 | }, 114 | }, 115 | }, 116 | }, 117 | tags: ['Cats'], 118 | }, 119 | patch: { 120 | operationId: 'CatsController_update', 121 | parameters: [], 122 | requestBody: { 123 | required: true, 124 | content: { 125 | 'application/json': { 126 | schema: { $ref: '#/components/schemas/UpdateCatDto' }, 127 | }, 128 | }, 129 | }, 130 | responses: { '200': { description: '' } }, 131 | tags: ['Cats'], 132 | }, 133 | }, 134 | '/cats/{id}': { 135 | get: { 136 | operationId: 'CatsController_findOne', 137 | parameters: [ 138 | { 139 | name: 'id', 140 | required: true, 141 | in: 'path', 142 | schema: { type: 'string' }, 143 | }, 144 | ], 145 | responses: { 146 | '201': { 147 | description: '', 148 | content: { 149 | 'application/json': { 150 | schema: { $ref: '#/components/schemas/CatDto' }, 151 | }, 152 | }, 153 | }, 154 | }, 155 | tags: ['Cats'], 156 | }, 157 | }, 158 | }, 159 | info: { 160 | title: 'Cats example', 161 | description: 'The cats API description', 162 | version: '1.0', 163 | contact: {}, 164 | }, 165 | tags: [{ name: 'cats', description: '' }], 166 | servers: [], 167 | components: { 168 | schemas: { 169 | GetCatsDto: { 170 | type: 'object', 171 | properties: { 172 | cats: { 173 | type: 'array', 174 | items: { type: 'string' }, 175 | description: 'List of cats', 176 | }, 177 | }, 178 | required: ['cats'], 179 | title: 'Get Cat Response', 180 | }, 181 | CatDto: { 182 | type: 'object', 183 | properties: { 184 | name: { type: 'string' }, 185 | age: { type: 'number' }, 186 | breed: { type: 'string' }, 187 | }, 188 | required: ['name', 'age', 'breed'], 189 | title: 'Cat', 190 | description: 'A cat', 191 | }, 192 | CreateCatResponseDto: { 193 | type: 'object', 194 | properties: { 195 | success: { type: 'boolean' }, 196 | message: { type: 'string' }, 197 | name: { type: 'string' }, 198 | }, 199 | required: ['success', 'message', 'name'], 200 | }, 201 | UpdateCatDto: { 202 | type: 'object', 203 | properties: { 204 | age: { type: 'number' }, 205 | breed: { type: 'string' }, 206 | }, 207 | required: ['age', 'breed'], 208 | }, 209 | }, 210 | }, 211 | }); 212 | }); 213 | 214 | afterAll(async () => { 215 | await app.close(); 216 | }); 217 | }); 218 | -------------------------------------------------------------------------------- /apps/example/src/main.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is not a production server yet! 3 | * This is only a minimal backend to get started. 4 | */ 5 | 6 | import { Logger } from '@nestjs/common'; 7 | import { NestFactory } from '@nestjs/core'; 8 | import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; 9 | import { AppModule } from './app/app.module'; 10 | import { patchNestjsSwagger } from '@anatine/zod-nestjs'; 11 | 12 | async function bootstrap() { 13 | const app = await NestFactory.create(AppModule); 14 | const globalPrefix = 'api'; 15 | app.setGlobalPrefix(globalPrefix); 16 | 17 | const config = new DocumentBuilder() 18 | .setTitle('Cats example') 19 | .setDescription('The cats API description') 20 | .setVersion('1.0') 21 | .setOpenAPIVersion('3.0.0') 22 | .addTag('cats') 23 | .build(); 24 | 25 | patchNestjsSwagger(); 26 | const document = SwaggerModule.createDocument(app, config); 27 | SwaggerModule.setup('api', app, document); 28 | 29 | const port = process.env.PORT || 3333; 30 | await app.listen(port, () => { 31 | Logger.log('Listening at http://localhost:' + port + '/' + globalPrefix); 32 | }); 33 | } 34 | 35 | bootstrap(); 36 | -------------------------------------------------------------------------------- /apps/example/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["node"], 7 | "emitDecoratorMetadata": true, 8 | "target": "es2015" 9 | }, 10 | "exclude": ["**/*.spec.ts", "**/*.test.ts", "jest.config.ts"], 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /apps/example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.app.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /apps/example/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["**/*.spec.ts", "**/*.test.ts", "**/*.d.ts", "jest.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /apps/example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { composePlugins, withNx } = require('@nx/webpack'); 2 | 3 | // Nx plugins for webpack. 4 | module.exports = composePlugins(withNx(), (config) => { 5 | // Update the webpack config as needed here. 6 | // e.g. `config.plugins.push(new MyPlugin())` 7 | return config; 8 | }); 9 | -------------------------------------------------------------------------------- /codegen.yml: -------------------------------------------------------------------------------- 1 | schema: './apps/codegen-e2e/example/*.graphql' 2 | generates: 3 | './apps/codegen-e2e/src/types.ts': 4 | plugins: 5 | - typescript 6 | 7 | # Configuration example for graphql-codegen-zod 8 | './apps/codegen-e2e/src/graphql-codegen-zod/output.ts': 9 | plugins: 10 | - ./dist/libs/graphql-codegen-zod/src/index.js 11 | config: 12 | allowEnumStringTypes: true 13 | onlyWithValidation: false 14 | lazy: true 15 | zodSchemasMap: 16 | EmailAddress: z.string().email() 17 | IPAddress: z.string() 18 | DateTime: z.string() 19 | JSON: z.object() 20 | Date: z.string() 21 | URL: z.string() 22 | zodTypesMap: 23 | EmailAddress: string 24 | 25 | # Configuration example for graphql-zod-validation 26 | './apps/codegen-e2e/src/graphql-zod-validation/zod-generated.ts': 27 | plugins: 28 | - './dist/libs/graphql-zod-validation/src/index.js' 29 | config: 30 | schema: zod 31 | importFrom: ../types 32 | useObjectTypes: true 33 | scalarSchemas: 34 | EmailAddress: z.string().email() 35 | IPAddress: z.string() 36 | DateTime: z.string() 37 | JSON: z.object() 38 | Date: z.string() 39 | URL: z.string() 40 | directives: 41 | # Write directives like 42 | # 43 | # directive: 44 | # arg1: schemaApi 45 | # arg2: ["schemaApi2", "Hello $1"] 46 | # 47 | # See more examples in `./tests/directive.spec.ts` 48 | # https://github.com/Code-Hex/graphql-codegen-typescript-validation-schema/blob/main/tests/directive.spec.ts 49 | constraint: 50 | minLength: min 51 | # Replace $1 with specified `startsWith` argument value of the constraint directive 52 | startsWith: ['regex', '/^$1/', 'message'] 53 | format: 54 | email: email 55 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import { getJestProjects } from '@nx/jest'; 2 | 3 | export default { 4 | projects: getJestProjects(), 5 | }; 6 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nx/jest/preset').default; 2 | 3 | module.exports = { 4 | ...nxPreset, 5 | /* TODO: Update to latest Jest snapshotFormat 6 | * By default Nx has kept the older style of Jest Snapshot formats 7 | * to prevent breaking of any existing tests with snapshots. 8 | * It's recommend you update to the latest format. 9 | * You can do this by removing snapshotFormat property 10 | * and running tests with --update-snapshot flag. 11 | * Example: "nx affected --targets=test --update-snapshot" 12 | * More info: https://jestjs.io/docs/upgrading-to-jest29#snapshot-format 13 | */ 14 | snapshotFormat: { escapeString: true, printBasicPrototype: true }, 15 | }; 16 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasksRunnerOptions": { 3 | "default": { 4 | "runner": "nx-cloud", 5 | "options": { 6 | "cacheableOperations": ["build", "lint", "test", "e2e"], 7 | "accessToken": "MTljMDM1ZmMtMjQxZi00YzQ2LTk2OGYtNTdhZTU2MmNmYjUzfHJlYWQtd3JpdGU=" 8 | } 9 | } 10 | }, 11 | "extends": "nx/presets/npm.json", 12 | "$schema": "./node_modules/nx/schemas/nx-schema.json", 13 | "affected": { 14 | "defaultBase": "main" 15 | }, 16 | "targetDependencies": { 17 | "version": [ 18 | { 19 | "target": "version", 20 | "projects": "dependencies" 21 | } 22 | ] 23 | }, 24 | "targetDefaults": { 25 | "test": { 26 | "inputs": ["default", "^default", "{workspaceRoot}/jest.preset.js"] 27 | }, 28 | "lint": { 29 | "inputs": ["default", "{workspaceRoot}/.eslintrc.json"] 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zod-plugins", 3 | "version": "5.0.0", 4 | "license": "MIT", 5 | "private": false, 6 | "description": "A collection of plugins for the Zod framework.", 7 | "author": { 8 | "name": "Brian McBride", 9 | "url": "https://www.linkedin.com/in/brianmcbride" 10 | }, 11 | "scripts": { 12 | "test": "jest", 13 | "affected:lint": "nx affected:lint", 14 | "affected:test": "nx affected:test", 15 | "affected:version": "nx affected --parallel=1 --base=last-release --target=version", 16 | "preaffected:version": "npm run affected:build", 17 | "affected:build": "nx affected:build", 18 | "affected:publish": "nx affected --target=publish", 19 | "affected:github": "nx affected --parallel=1 --target=github" 20 | }, 21 | "dependencies": { 22 | "@graphql-codegen/plugin-helpers": "^4.2.0", 23 | "@graphql-codegen/schema-ast": "^3.0.1", 24 | "@graphql-codegen/visitor-plugin-common": "^3.1.1", 25 | "@graphql-tools/utils": "^10.0.1", 26 | "axios": "^1.4.0", 27 | "randexp": "^0.5.3", 28 | "ts-deepmerge": "^6.1.0", 29 | "tslib": "^2.6.0" 30 | }, 31 | "devDependencies": { 32 | "@faker-js/faker": "^9.6.0", 33 | "@graphql-codegen/cli": "^5.0.5", 34 | "@graphql-codegen/typescript": "^3.0.4", 35 | "@graphql-codegen/typescript-operations": "^3.0.4", 36 | "@jscutlery/semver": "^3.0.0", 37 | "@nestjs/common": "11.0.11", 38 | "@nestjs/core": "11.0.11", 39 | "@nestjs/platform-express": "11.0.11", 40 | "@nestjs/schematics": "11.0.2", 41 | "@nestjs/swagger": "11.0.6", 42 | "@nestjs/testing": "11.0.11", 43 | "@nx/eslint-plugin": "16.4.1", 44 | "@nx/jest": "16.4.1", 45 | "@nx/js": "16.4.1", 46 | "@nx/linter": "16.4.1", 47 | "@nx/nest": "16.4.1", 48 | "@nx/node": "16.4.1", 49 | "@nx/webpack": "16.4.1", 50 | "@nx/workspace": "16.4.1", 51 | "@swc/core": "^1.3.67", 52 | "@swc/jest": "0.2.26", 53 | "@types/isomorphic-form-data": "^2.0.1", 54 | "@types/jest": "29.5.2", 55 | "@types/node": "20.3.2", 56 | "@types/supertest": "^2.0.12", 57 | "@types/validator": "^13.7.17", 58 | "@types/ws": "^8.5.5", 59 | "@typescript-eslint/eslint-plugin": "5.60.1", 60 | "@typescript-eslint/experimental-utils": "5.60.1", 61 | "@typescript-eslint/parser": "5.60.1", 62 | "dotenv": "16.3.1", 63 | "eslint": "8.43.0", 64 | "eslint-config-prettier": "8.8.0", 65 | "graphql": "^16.7.1", 66 | "jest": "29.5.0", 67 | "jest-environment-node": "^29.5.0", 68 | "ngx-deploy-npm": "8.0.0", 69 | "nx": "16.4.1", 70 | "nx-cloud": "^19.1.0", 71 | "openapi3-ts": "^4.1.2", 72 | "prettier": "^2.8.8", 73 | "reflect-metadata": "^0.1.13", 74 | "supertest": "^6.3.3", 75 | "ts-jest": "29.1.1", 76 | "ts-node": "10.9.1", 77 | "tslib": "^2.6.0", 78 | "typescript": "5.1.6", 79 | "validator": "^13.9.0", 80 | "zod": "^3.22.4" 81 | }, 82 | "workspaces": [ 83 | "packages/**" 84 | ] 85 | } 86 | -------------------------------------------------------------------------------- /packages/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anatine/zod-plugins/819fbec3eb95ee17a6ad8076341213417e736736/packages/.gitkeep -------------------------------------------------------------------------------- /packages/graphql-codegen-zod/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@nx/js/babel", 5 | { 6 | "useBuiltIns": "usage" 7 | } 8 | ] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /packages/graphql-codegen-zod/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/graphql-codegen-zod/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). 4 | 5 | ### [0.4.1](https://github.com/anatine/zod-plugins/compare/graphql-codegen-zod-0.4.0...graphql-codegen-zod-0.4.1) (2024-03-19) 6 | 7 | ## [0.4.0](https://github.com/anatine/zod-plugins/compare/graphql-codegen-zod-0.3.14...graphql-codegen-zod-0.4.0) (2022-12-12) 8 | 9 | 10 | ### Features 11 | 12 | * Updated Dependencies ([ad8cfc8](https://github.com/anatine/zod-plugins/commit/ad8cfc8fa40ca32736dbfb0d8906569d2a626cbe)) 13 | 14 | ### [0.3.14](https://github.com/anatine/zod-plugins/compare/graphql-codegen-zod-0.3.13...graphql-codegen-zod-0.3.14) (2022-07-26) 15 | 16 | 17 | ### Bug Fixes 18 | 19 | * missing readme ([581e371](https://github.com/anatine/zod-plugins/commit/581e37112c223782759635ae34937a0dfa664dc9)) 20 | * more pipeline work ([bc6c615](https://github.com/anatine/zod-plugins/commit/bc6c6153627bfafbcac95487f4de1925e10a47b6)) 21 | * more readme fixes ([ed36d93](https://github.com/anatine/zod-plugins/commit/ed36d935dc6bb93ab35b5212e966130ff3ba9838)) 22 | 23 | ## [0.3.14](https://github.com/anatine/zod-plugins/compare/graphql-codegen-zod-0.3.13...graphql-codegen-zod-0.3.14) (2022-07-26) 24 | 25 | 26 | ### Bug Fixes 27 | 28 | * missing readme ([581e371](https://github.com/anatine/zod-plugins/commit/581e37112c223782759635ae34937a0dfa664dc9)) 29 | * more pipeline work ([bc6c615](https://github.com/anatine/zod-plugins/commit/bc6c6153627bfafbcac95487f4de1925e10a47b6)) 30 | * more readme fixes ([ed36d93](https://github.com/anatine/zod-plugins/commit/ed36d935dc6bb93ab35b5212e966130ff3ba9838)) 31 | 32 | 33 | 34 | ## [0.3.13](https://github.com/anatine/zod-plugins/compare/graphql-codegen-zod-0.3.12...graphql-codegen-zod-0.3.13) (2022-07-25) 35 | 36 | 37 | 38 | ## [0.3.12](https://github.com/anatine/zod-plugins/compare/graphql-codegen-zod-0.3.11...graphql-codegen-zod-0.3.12) (2022-07-25) 39 | 40 | 41 | 42 | ## [0.3.11](https://github.com/anatine/zod-plugins/compare/graphql-codegen-zod-0.3.10...graphql-codegen-zod-0.3.11) (2022-07-25) 43 | 44 | 45 | 46 | ## [0.3.10](https://github.com/anatine/zod-plugins/compare/graphql-codegen-zod-0.3.9...graphql-codegen-zod-0.3.10) (2022-07-25) 47 | 48 | 49 | 50 | ## [0.3.9](https://github.com/anatine/zod-plugins/compare/graphql-codegen-zod-0.3.8...graphql-codegen-zod-0.3.9) (2022-07-25) 51 | 52 | 53 | ### Bug Fixes 54 | 55 | * Nx pipeline work ([9012905](https://github.com/anatine/zod-plugins/commit/90129055519d329831d026757e04b8192376b6a9)) 56 | 57 | 58 | 59 | ## [0.3.8](https://github.com/anatine/zod-plugins/compare/graphql-codegen-zod-0.3.7...graphql-codegen-zod-0.3.8) (2022-07-25) 60 | 61 | 62 | 63 | ## [0.3.7](https://github.com/anatine/zod-plugins/compare/graphql-codegen-zod-0.3.6...graphql-codegen-zod-0.3.7) (2022-07-25) 64 | 65 | 66 | 67 | ## [0.3.6](https://github.com/anatine/zod-plugins/compare/graphql-codegen-zod-0.3.5...graphql-codegen-zod-0.3.6) (2022-07-25) 68 | 69 | 70 | 71 | ## [0.3.5](https://github.com/anatine/zod-plugins/compare/graphql-codegen-zod-0.3.4...graphql-codegen-zod-0.3.5) (2022-07-25) 72 | 73 | 74 | 75 | ## [0.3.4](https://github.com/anatine/zod-plugins/compare/graphql-codegen-zod-0.3.3...graphql-codegen-zod-0.3.4) (2022-07-25) 76 | 77 | 78 | 79 | ## [0.3.3](https://github.com/anatine/zod-plugins/compare/graphql-codegen-zod-0.3.2...graphql-codegen-zod-0.3.3) (2022-07-24) 80 | 81 | 82 | ### Bug Fixes 83 | 84 | * Missing README after refactor ([00ceb10](https://github.com/anatine/zod-plugins/commit/00ceb10be8251c6be2a83e64a9a8cd6116451938)) 85 | 86 | 87 | 88 | ## [0.3.2](https://github.com/anatine/zod-plugins/compare/graphql-codegen-zod-0.3.1...graphql-codegen-zod-0.3.2) (2022-07-24) 89 | 90 | 91 | 92 | # [0.2.0](https://github.com/anatine/zod-plugins/compare/graphql-codegen-zod-0.1.0...graphql-codegen-zod-0.2.0) (2022-07-24) 93 | 94 | # [0.1.0](https://github.com/anatine/zod-plugins/compare/graphql-codegen-zod-0.0.7...graphql-codegen-zod-0.1.0) (2022-07-14) 95 | 96 | ## [0.0.7](https://github.com/anatine/zod-plugins/compare/graphql-codegen-zod-0.0.6...graphql-codegen-zod-0.0.7) (2022-07-14) 97 | 98 | ### Bug Fixes 99 | 100 | * release tags broken with new CI/CD ([451a761](https://github.com/anatine/zod-plugins/commit/451a7614564fa214a5a39137ac8c38beacfcf970)) 101 | 102 | ## [0.0.3](https://github.com/anatine/zod-plugins/compare/graphql-codegen-zod-0.0.2...graphql-codegen-zod-0.0.3) (2022-07-14) 103 | 104 | ## [0.0.2](https://github.com/anatine/zod-plugins/compare/graphql-codegen-zod-0.0.1...graphql-codegen-zod-0.0.2) (2022-07-14) 105 | 106 | ### Bug Fixes 107 | 108 | * Adding in new release githuib actions ([29a2455](https://github.com/anatine/zod-plugins/commit/29a2455161f7021df9f933d0d8b200a08fe31fde)) 109 | * Update to new code deps ([d771c4b](https://github.com/anatine/zod-plugins/commit/d771c4b2b026635a6704eeb1fca80dd2f2e5e8e8)) 110 | -------------------------------------------------------------------------------- /packages/graphql-codegen-zod/LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2022 Brian McBride 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 | -------------------------------------------------------------------------------- /packages/graphql-codegen-zod/README.md: -------------------------------------------------------------------------------- 1 | # GraphQL codegen schema to Zod schema 2 | 3 | The reason for this project is to maintain a single source of truth, between graphql and zod. This is useful for validation on forms. Inspired by [codegen-graphql-yup](https://github.com/tinezmatias/codegen-graphql-yup) 4 | 5 | ## Configs 6 | 7 | - onlyWithValidation: boolean ( default false) If you want to generate an schema for all your input objects definitions with or without the directive put it in true. 8 | - zodSchemasMap: a map of your scalars to a zod type. This is useful for scalars such as EmailAddress that are `z.string().email()` 9 | 10 | ### Simple use 11 | 12 | If you only want to validate the required fields, what you can do is use the plugin in the following way 13 | 14 | ```yaml 15 | generates: 16 | schemas.ts: 17 | plugins: 18 | - @anatine/graphql-codegen-zod 19 | ``` 20 | 21 | ### Full Use 22 | 23 | If you need more validations than only required fields, you have to follow this steps. 24 | 25 | this is because in graphql instrospection we dont have access to directives 26 | 27 | - Add the directive in your schema. 28 | - In codegen.yml config use that file like schema. 29 | 30 | ### Directive Schema 31 | 32 | ```graphql 33 | directive @validation( 34 | pattern: String 35 | min: Int 36 | max: Int 37 | requiredMessage: String 38 | typeOf: String 39 | ) on INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION 40 | ``` 41 | 42 | ## Example 43 | 44 | result.graphql 45 | 46 | ```graphql 47 | directive @validation( 48 | pattern: String 49 | min: Int 50 | max: Int 51 | requiredMessage: String 52 | typeOf: String 53 | ) on INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION 54 | enum TestEnum { 55 | ENUM1 56 | ENUM2 57 | } 58 | type TestSampleType { 59 | id: ID! 60 | name: String! 61 | email: String 62 | } 63 | input TestInput { 64 | string: String 65 | stringRequired: String! 66 | enum: TestEnum 67 | enumRequired: TestEnum! 68 | scalar: EmailAddress 69 | scalarRequired: EmailAddress! 70 | enumArray: [TestEnum] 71 | enumArrayRequired: [TestEnum]! 72 | emailArray: [EmailAddress] 73 | emailArrayRequired: [EmailAddress]! 74 | emailRequiredArray: [EmailAddress!] 75 | emailRequiredArrayRequired: [EmailAddress!]! 76 | stringArray: [String] 77 | stringArrayRequired: [String]! 78 | stringRequiredArray: [String!] 79 | stringRequiredArrayRequired: [String!]! 80 | } 81 | input RegisterAddressInput { 82 | postalCode: TestInput! 83 | state: [String]! 84 | city: String! 85 | someNumber: Int @validation(min: 10, max: 20) 86 | someNumberFloat: Float @validation(min: 10.50, max: 20.50) 87 | someBoolean: Boolean 88 | ipAddress: IPAddress 89 | line2: String @validation(min: 10, max: 20) 90 | } 91 | scalar EmailAddress 92 | scalar IPAddress 93 | ``` 94 | 95 | codegen.yml 96 | 97 | ```yaml 98 | schema: './apps/codegen-e2e/src/*.graphql' 99 | generates: 100 | './apps/codegen-e2e/generated/output.ts': 101 | plugins: 102 | - @anatine/graphql-codegen-zod 103 | config: 104 | allowEnumStringTypes: true 105 | onlyWithValidation: false 106 | lazy: true 107 | zodSchemasMap: 108 | EmailAddress: z.string().email() 109 | IPAddress: z.string() 110 | DateTime: z.string() 111 | JSON: z.object() 112 | Date: z.string() 113 | zodTypesMap: 114 | EmailAddress: string 115 | ``` 116 | -------------------------------------------------------------------------------- /packages/graphql-codegen-zod/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'graphql-codegen-zod', 4 | preset: '../../jest.preset.js', 5 | globals: {}, 6 | testEnvironment: 'node', 7 | transform: { 8 | '^.+\\.[tj]sx?$': [ 9 | 'ts-jest', 10 | { 11 | tsconfig: '/tsconfig.spec.json', 12 | }, 13 | ], 14 | }, 15 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 16 | coverageDirectory: '../../coverage/packages/graphql-codegen-zod', 17 | }; 18 | -------------------------------------------------------------------------------- /packages/graphql-codegen-zod/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@anatine/graphql-codegen-zod", 3 | "version": "0.4.1", 4 | "description": "Its a library to parse from Graphql Schema to a Zod Schema", 5 | "license": "MIT", 6 | "public": true, 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/anatine/zod-plugins" 10 | }, 11 | "homepage": "https://github.com/anatine/zod-plugins/tree/main/packages/graphql-codegen-zod", 12 | "author": { 13 | "name": "Brian McBride", 14 | "url": "https://www.linkedin.com/in/brianmcbride" 15 | }, 16 | "contributors": [ 17 | { 18 | "name": "withshepherd", 19 | "url": "https://github.com/withshepherd/graphql-codegen-zod/" 20 | } 21 | ], 22 | "keywords": [ 23 | "zod", 24 | "graphql", 25 | "graphql-codegen" 26 | ], 27 | "dependencies": { 28 | "@graphql-tools/utils": "^8.8.0" 29 | }, 30 | "peerDependencies": { 31 | "zod": "^3.17.3", 32 | "@graphql-tools/utils": "^8.8.0", 33 | "graphql-code-generator": "^0.18.2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/graphql-codegen-zod/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-codegen-zod", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "packages/graphql-codegen-zod/src", 5 | "projectType": "library", 6 | "targets": { 7 | "build": { 8 | "executor": "@nx/js:tsc", 9 | "outputs": ["{options.outputPath}"], 10 | "options": { 11 | "outputPath": "dist/packages/graphql-codegen-zod", 12 | "tsConfig": "packages/graphql-codegen-zod/tsconfig.lib.json", 13 | "packageJson": "packages/graphql-codegen-zod/package.json", 14 | "main": "packages/graphql-codegen-zod/src/index.ts", 15 | "assets": ["packages/graphql-codegen-zod/*.md"] 16 | } 17 | }, 18 | "lint": { 19 | "executor": "@nx/linter:eslint", 20 | "outputs": ["{options.outputFile}"], 21 | "options": { 22 | "lintFilePatterns": ["packages/graphql-codegen-zod/**/*.ts"] 23 | } 24 | }, 25 | "test": { 26 | "executor": "@nx/jest:jest", 27 | "outputs": ["{workspaceRoot}/coverage/packages/graphql-codegen-zod"], 28 | "options": { 29 | "jestConfig": "packages/graphql-codegen-zod/jest.config.ts", 30 | "passWithNoTests": true 31 | } 32 | }, 33 | "version": { 34 | "executor": "@jscutlery/semver:version", 35 | "options": { 36 | "push": true, 37 | "preset": "conventional", 38 | "skipCommitTypes": ["ci"], 39 | "postTargets": [ 40 | "graphql-codegen-zod:build", 41 | "graphql-codegen-zod:publish", 42 | "graphql-codegen-zod:github" 43 | ] 44 | } 45 | }, 46 | "github": { 47 | "executor": "@jscutlery/semver:github", 48 | "options": { 49 | "tag": "${tag}", 50 | "notes": "${notes}" 51 | } 52 | }, 53 | "publish": { 54 | "executor": "ngx-deploy-npm:deploy", 55 | "options": { 56 | "access": "public", 57 | "distFolderPath": "dist/packages/graphql-codegen-zod" 58 | }, 59 | "dependsOn": ["build"] 60 | } 61 | }, 62 | "tags": [] 63 | } 64 | -------------------------------------------------------------------------------- /packages/graphql-codegen-zod/src/handlers/directives/index.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveNode } from 'graphql'; 2 | import { DIRECTIVE_NAME } from '../../utils/constants'; 3 | 4 | const directiveHandler = ( 5 | directives: readonly DirectiveNode[] = [], 6 | ): { requiredMessage: string | undefined; extraValidations: Record } => { 7 | let result: any = {}; 8 | const directive = directives.find((directive) => directive.name.value === DIRECTIVE_NAME); 9 | 10 | if (directive) { 11 | directive.arguments?.forEach((item) => { 12 | let value = (item.value as any).value; 13 | 14 | if (item.value.kind === 'IntValue') { 15 | value = parseInt(value); 16 | } 17 | 18 | if (item.value.kind === 'FloatValue') { 19 | value = parseFloat(value); 20 | } 21 | 22 | result[item.name.value] = value; 23 | }); 24 | } 25 | 26 | if ('requiredMessage' in result) { 27 | const { requiredMessage, ...extraValidations } = result; 28 | result = { requiredMessage, extraValidations }; 29 | } else { 30 | result = { extraValidations: result }; 31 | } 32 | 33 | return result; 34 | }; 35 | 36 | export default directiveHandler; 37 | -------------------------------------------------------------------------------- /packages/graphql-codegen-zod/src/handlers/enumsHandler.ts: -------------------------------------------------------------------------------- 1 | import { IConfig, IEnums, ITypes } from '../types'; 2 | 3 | const enumsHandler = (enums: IEnums, types: ITypes, config: IConfig): string => { 4 | return Object.keys(enums) 5 | .map((key) => { 6 | let schemaName = `export const ${key}Schema`; 7 | 8 | if (types[key]) { 9 | schemaName += `: z.ZodSchema<${types[key]}>`; 10 | // } else if (config.importOperationTypesFrom) { 11 | // schemaName += `: z.ZodSchema<${['`', '${Types.', key, '}`'].join('')}>`; 12 | // } else { 13 | // schemaName += `: z.ZodSchema<${['`', '${', key, '}`'].join('')}>`; 14 | } 15 | 16 | return `${schemaName} = z.enum(${JSON.stringify(enums[key])});`; 17 | }) 18 | .join('\n'); 19 | }; 20 | 21 | export default enumsHandler; 22 | -------------------------------------------------------------------------------- /packages/graphql-codegen-zod/src/handlers/fields/fieldHandlers.ts: -------------------------------------------------------------------------------- 1 | import { FieldDefinitionNode, InputValueDefinitionNode } from 'graphql'; 2 | import { IConfig } from '../../types'; 3 | import directiveHandler from '../directives/index'; 4 | import fieldKindHandler from './kindsHandler'; 5 | 6 | const fieldHandler = ( 7 | field: InputValueDefinitionNode | FieldDefinitionNode, 8 | config: IConfig 9 | ) => { 10 | const fieldName = field.name.value; 11 | const fieldType = field.type; 12 | 13 | const { extraValidations } = directiveHandler(field.directives); 14 | let extra = ''; 15 | 16 | for (const key in extraValidations) { 17 | if (Object.prototype.hasOwnProperty.call(extraValidations, key)) { 18 | const value = extraValidations[key]; 19 | if (typeof value === 'string') { 20 | extra = `${extra}.${key}('${value}')`; 21 | } else { 22 | extra = `${extra}.${key}(${value})`; 23 | } 24 | } 25 | } 26 | return `${fieldName}: ${fieldKindHandler({ 27 | fieldName, 28 | type: fieldType, 29 | extra, 30 | isOptional: true, 31 | })}`; 32 | }; 33 | 34 | export default fieldHandler; 35 | -------------------------------------------------------------------------------- /packages/graphql-codegen-zod/src/handlers/fields/fieldsHandler.ts: -------------------------------------------------------------------------------- 1 | import { FieldDefinitionNode, InputValueDefinitionNode } from 'graphql'; 2 | import { IConfig } from '../../types'; 3 | import fieldHandler from './fieldHandlers'; 4 | 5 | const fieldsHandler = ( 6 | fields: (InputValueDefinitionNode | FieldDefinitionNode)[], 7 | config: IConfig 8 | ) => { 9 | return fields.map((field) => fieldHandler(field, config)).join(',\n'); 10 | }; 11 | 12 | export default fieldsHandler; 13 | -------------------------------------------------------------------------------- /packages/graphql-codegen-zod/src/handlers/fields/index.ts: -------------------------------------------------------------------------------- 1 | import fieldsHandler from './fieldsHandler'; 2 | 3 | export default fieldsHandler -------------------------------------------------------------------------------- /packages/graphql-codegen-zod/src/handlers/fields/kindsHandler.ts: -------------------------------------------------------------------------------- 1 | import { NamedTypeNode, TypeNode } from 'graphql'; 2 | import { isNamed } from '../../types/index'; 3 | import { isArray, isRequired, isType } from '../../utils/typesCheckers'; 4 | import fieldNamedTypeHandler from './namedTypesHandlers'; 5 | 6 | const fieldKindHandler = ({ 7 | fieldName, 8 | type, 9 | extra = '', 10 | isOptional = true, 11 | }: { 12 | fieldName: string; 13 | type: NamedTypeNode | TypeNode; 14 | extra: string; 15 | isOptional: boolean; 16 | }) => { 17 | let result = ''; 18 | if (fieldName === 'arrayRequired') { 19 | console.log({ fieldName, type, extra, isOptional }); 20 | } 21 | if (isRequired(type.kind) && 'type' in type) { 22 | result = `${fieldKindHandler({ 23 | type: type.type, 24 | fieldName, 25 | extra, 26 | isOptional: false, 27 | })}`; 28 | } 29 | 30 | if (isArray(type.kind) && 'type' in type) { 31 | result = `z.array(${fieldKindHandler({ 32 | type: type.type, 33 | fieldName, 34 | extra, 35 | isOptional: true, 36 | })})`; 37 | 38 | if (isOptional) { 39 | result = `${result}.nullish()`; 40 | } 41 | } 42 | 43 | if (isType(type.kind) && isNamed(type)) { 44 | result = fieldNamedTypeHandler(type.name.value); 45 | result = `${result}${extra}`; 46 | 47 | if (isOptional) { 48 | result = `${result}.nullish()`; 49 | } 50 | } 51 | 52 | return result; 53 | }; 54 | 55 | export default fieldKindHandler; 56 | -------------------------------------------------------------------------------- /packages/graphql-codegen-zod/src/handlers/fields/namedTypesHandlers.ts: -------------------------------------------------------------------------------- 1 | import { isBoolean, isNumber, isRef, isString } from '../../utils/typesCheckers'; 2 | 3 | const fieldNamedTypeHandler = (type: string) => { 4 | let result = 'z.'; 5 | 6 | if (isRef(type)) { 7 | result = type + 'Schema'; 8 | } else if (isBoolean(type)) { 9 | result = result + 'boolean()'; 10 | } else if (isString(type)) { 11 | result = result + 'string()'; 12 | } else if (isNumber(type)) { 13 | result = result + 'number()'; 14 | } else { 15 | // Assume it's a defined schema! 16 | result = type + 'Schema'; 17 | } 18 | 19 | return result; 20 | }; 21 | 22 | export default fieldNamedTypeHandler; 23 | -------------------------------------------------------------------------------- /packages/graphql-codegen-zod/src/handlers/nodesHandler.ts: -------------------------------------------------------------------------------- 1 | import { IConfig, INodes, ITypes } from '../types/index'; 2 | import fieldsHandler from './fields'; 3 | 4 | const nodesHandler = (nodes: INodes[], config: IConfig, types: ITypes) => { 5 | return nodes 6 | .map(({ name, fields }) => { 7 | const fieldsZod = fieldsHandler(fields, config); 8 | let schemaName = `export const ${name}Schema`; 9 | 10 | if (types[name]) { 11 | schemaName += `: z.ZodSchema<${types[name]}>`; 12 | // } else if (config.importOperationTypesFrom) { 13 | // schemaName += `: z.ZodSchema`; 14 | // } else { 15 | // schemaName += `: z.ZodSchema<${name}>`; 16 | } 17 | 18 | if (config.lazy) { 19 | return `${schemaName} = z.lazy(() => z.object({\n${fieldsZod}\n}))`; 20 | } 21 | 22 | return `${schemaName} = z.object({\n${fieldsZod}\n})`; 23 | }) 24 | .join(';\n\n\n'); 25 | }; 26 | 27 | export default nodesHandler; 28 | -------------------------------------------------------------------------------- /packages/graphql-codegen-zod/src/handlers/scalarsHandler.ts: -------------------------------------------------------------------------------- 1 | import { IConfig, IScalars, ITypes } from '../types'; 2 | 3 | const scalarsHandler = (scalars: IScalars, types: ITypes, config: IConfig): string => { 4 | return Object.keys(scalars) 5 | .map((key) => { 6 | let schemaName = `export const ${key}Schema`; 7 | 8 | if (types[key]) { 9 | schemaName += `: z.ZodSchema<${types[key]}>`; 10 | // } else if (config.importOperationTypesFrom) { 11 | // schemaName += `: z.ZodSchema`; 12 | // } else { 13 | // schemaName += `: z.ZodSchema`; 14 | } 15 | 16 | return `${schemaName} = ${scalars[key]};`; 17 | }) 18 | .join('\n'); 19 | }; 20 | 21 | export default scalarsHandler; 22 | -------------------------------------------------------------------------------- /packages/graphql-codegen-zod/src/handlers/schemaHandler.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { printSchemaWithDirectives } from '@graphql-tools/utils'; 3 | import { GraphQLSchema, parse, visit } from 'graphql'; 4 | import { inspect } from 'util'; 5 | import { IConfig, IEnums, INodes, IScalars, ITypes } from '../types/index'; 6 | import { DIRECTIVE_NAME } from '../utils/constants'; 7 | 8 | const schemaHandler = (schema: GraphQLSchema, config: IConfig) => { 9 | const printedSchema = printSchemaWithDirectives(schema); 10 | 11 | const astNode = parse(printedSchema); 12 | 13 | const nodes: INodes[] = []; 14 | const enums: IEnums = {}; 15 | const scalars: IScalars = {}; 16 | const types: ITypes = {}; 17 | 18 | visit(astNode, { 19 | InputObjectTypeDefinition: { 20 | leave: (node) => { 21 | // console.log( 22 | // inspect({ source: 'InputObjectTypeDefinition', node }, false, 5, true) 23 | // ); 24 | let hasValidation = Boolean(!config.onlyWithValidation); 25 | if (!hasValidation) { 26 | node.fields?.forEach((field) => { 27 | const validation = field.directives?.find( 28 | (directive) => directive.name.value === DIRECTIVE_NAME 29 | ); 30 | if (validation) { 31 | hasValidation = true; 32 | } 33 | }); 34 | } 35 | if (hasValidation) 36 | nodes.push({ 37 | name: node.name.value, 38 | fields: [...(node.fields || [])], 39 | }); 40 | 41 | if (config.zodTypesMap[node.name.value]) { 42 | types[node.name.value] = config.zodTypesMap[node.name.value]; 43 | } 44 | }, 45 | }, 46 | ObjectTypeDefinition: { 47 | leave: (node) => { 48 | // console.log( 49 | // inspect({ source: 'ObjectTypeDefinition', node }, false, 5, true) 50 | // ); 51 | let hasValidation = Boolean(!config.onlyWithValidation); 52 | if (!hasValidation) { 53 | node.fields?.forEach((field) => { 54 | const validation = field.directives?.find( 55 | (directive) => directive.name.value === DIRECTIVE_NAME 56 | ); 57 | if (validation) { 58 | hasValidation = true; 59 | } 60 | }); 61 | } 62 | if (hasValidation) { 63 | console.log(`Pushing ${node.name.value}`); 64 | nodes.push({ 65 | name: node.name.value, 66 | fields: [...(node.fields || [])], 67 | }); 68 | } 69 | 70 | console.log(`types: ${node.name.value}`); 71 | if (config.zodTypesMap[node.name.value]) { 72 | types[node.name.value] = config.zodTypesMap[node.name.value]; 73 | } 74 | }, 75 | }, 76 | EnumTypeDefinition: { 77 | leave: (node) => { 78 | if (node.values) { 79 | enums[node.name.value] = node.values?.map((e) => e.name.value); 80 | } 81 | }, 82 | }, 83 | ScalarTypeDefinition: { 84 | leave: (node) => { 85 | if (config.zodSchemasMap[node.name.value]) { 86 | scalars[node.name.value] = config.zodSchemasMap[node.name.value]; 87 | } else { 88 | throw new Error( 89 | `${node.name.value} is not a defined scalar. Please define it in the codegen.yml file` 90 | ); 91 | } 92 | 93 | if (config.zodTypesMap[node.name.value]) { 94 | types[node.name.value] = config.zodTypesMap[node.name.value]; 95 | } 96 | }, 97 | }, 98 | }); 99 | 100 | return { 101 | hasValidation: !config.onlyWithValidation, 102 | nodes, 103 | enums, 104 | scalars, 105 | types, 106 | }; 107 | }; 108 | 109 | export default schemaHandler; 110 | -------------------------------------------------------------------------------- /packages/graphql-codegen-zod/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { PluginFunction } from '@graphql-codegen/plugin-helpers'; 2 | import enumsHandler from './handlers/enumsHandler'; 3 | import nodesHandler from './handlers/nodesHandler'; 4 | import scalarsHandler from './handlers/scalarsHandler'; 5 | import schemaHandler from './handlers/schemaHandler'; 6 | 7 | export const plugin: PluginFunction = (schema, documents, config) => { 8 | // const results = schemaHandler(schema, config); 9 | 10 | // return `export const result = ${JSON.stringify( 11 | // { 12 | // results, 13 | // }, 14 | // null, 15 | // 2 16 | // )}`; 17 | 18 | const { enums, nodes, scalars, types } = schemaHandler(schema, config); 19 | const parsedEnums = enumsHandler(enums, types, config); 20 | const parsedNodes = nodesHandler(nodes, config, types); 21 | const parsedScalars = scalarsHandler(scalars, types, config); 22 | 23 | return [ 24 | `import { z } from 'zod';`, 25 | config.importOperationTypesFrom 26 | ? `import type * as Types from '${config.importOperationTypesFrom}'` 27 | : '', 28 | parsedScalars, 29 | parsedEnums, 30 | parsedNodes, 31 | ].join('\n\n'); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/graphql-codegen-zod/src/types/graphqlTypes.ts: -------------------------------------------------------------------------------- 1 | // THIS ARE TYPES IN THE CONTEXT OF THE FIELD 2 | export const TYPE_LIST = 'ListType'; 3 | export const TYPE_NONULL = 'NonNullType'; 4 | export const TYPE_NAMED = 'NamedType'; 5 | 6 | // THIS ARE TYPES OF FIELD VALUE 7 | export const TYPE_INPUT = 'Input'; 8 | export const TYPE_BOOLEAN = 'Boolean'; 9 | export const TYPE_STRINGS = ['ID', 'String'] 10 | export const TYPE_NUMBERS = ['Int', 'Float'] -------------------------------------------------------------------------------- /packages/graphql-codegen-zod/src/types/index.ts: -------------------------------------------------------------------------------- 1 | // TYPES 2 | import type { 3 | FieldDefinitionNode, 4 | InputValueDefinitionNode, 5 | ObjectTypeDefinitionNode, 6 | } from 'graphql'; 7 | import { NamedTypeNode, TypeNode } from 'graphql'; 8 | 9 | export interface INodes { 10 | name: string; 11 | fields: (InputValueDefinitionNode | FieldDefinitionNode)[]; 12 | } 13 | 14 | export interface IEnums { 15 | [key: string]: string[]; 16 | } 17 | export interface IScalars { 18 | [key: string]: string; 19 | } 20 | 21 | export interface ITypes { 22 | [key: string]: string; 23 | } 24 | 25 | export interface IHandled { 26 | nodes: INodes[]; 27 | enums: IEnums; 28 | } 29 | 30 | export interface IConfig { 31 | defaultRequiredMessage?: string; 32 | onlyWithValidation?: boolean; 33 | zodSchemasMap: { [key: string]: string }; 34 | zodTypesMap: { [key: string]: string }; 35 | lazy?: boolean; 36 | importOperationTypesFrom?: string; 37 | } 38 | 39 | export const isNamed = ( 40 | type: NamedTypeNode | TypeNode 41 | ): type is NamedTypeNode => { 42 | return (type as NamedTypeNode).name !== undefined; 43 | }; 44 | -------------------------------------------------------------------------------- /packages/graphql-codegen-zod/src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const DIRECTIVE_NAME = 'validation'; 2 | -------------------------------------------------------------------------------- /packages/graphql-codegen-zod/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './typesCheckers' 2 | export * from './constants' -------------------------------------------------------------------------------- /packages/graphql-codegen-zod/src/utils/typesCheckers.ts: -------------------------------------------------------------------------------- 1 | // CONSTANTS 2 | import { 3 | TYPE_BOOLEAN, 4 | TYPE_INPUT, 5 | TYPE_LIST, 6 | TYPE_NAMED, 7 | TYPE_NONULL, 8 | TYPE_NUMBERS, 9 | TYPE_STRINGS, 10 | } from '../types/graphqlTypes'; 11 | 12 | export const isArray = (kind: string) => kind === TYPE_LIST; 13 | export const isRequired = (kind: string) => kind === TYPE_NONULL; 14 | export const isType = (kind: string) => kind === TYPE_NAMED; 15 | 16 | export const isRef = (kind: string) => kind.includes(TYPE_INPUT); 17 | export const isBoolean = (kind: string) => kind === TYPE_BOOLEAN; 18 | export const isString = (kind: string) => TYPE_STRINGS.includes(kind); 19 | export const isNumber = (kind: string) => TYPE_NUMBERS.includes(kind); 20 | -------------------------------------------------------------------------------- /packages/graphql-codegen-zod/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ], 13 | "compilerOptions": { 14 | "forceConsistentCasingInFileNames": true, 15 | "strict": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/graphql-codegen-zod/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "../../dist/out-tsc", 6 | "declaration": true, 7 | "types": ["node"] 8 | }, 9 | "exclude": ["jest.config.ts", "**/*.spec.ts", "**/*.test.ts"], 10 | "include": ["**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/graphql-codegen-zod/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "jest.config.ts", 10 | "**/*.test.ts", 11 | "**/*.spec.ts", 12 | "**/*.test.tsx", 13 | "**/*.spec.tsx", 14 | "**/*.test.js", 15 | "**/*.spec.js", 16 | "**/*.test.jsx", 17 | "**/*.spec.jsx", 18 | "**/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /packages/graphql-zod-validation/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@nx/js/babel", 5 | { 6 | "useBuiltIns": "usage" 7 | } 8 | ] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /packages/graphql-zod-validation/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/graphql-zod-validation/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). 4 | 5 | ### [0.9.1](https://github.com/anatine/zod-plugins/compare/graphql-zod-validation-0.9.0...graphql-zod-validation-0.9.1) (2024-03-19) 6 | 7 | ## [0.9.0](https://github.com/anatine/zod-plugins/compare/graphql-zod-validation-0.8.4...graphql-zod-validation-0.9.0) (2022-12-12) 8 | 9 | 10 | ### Features 11 | 12 | * Updated Dependencies ([ad8cfc8](https://github.com/anatine/zod-plugins/commit/ad8cfc8fa40ca32736dbfb0d8906569d2a626cbe)) 13 | 14 | ### [0.8.4](https://github.com/anatine/zod-plugins/compare/graphql-zod-validation-0.8.3...graphql-zod-validation-0.8.4) (2022-07-26) 15 | 16 | ## [0.8.3](https://github.com/anatine/zod-plugins/compare/graphql-zod-validation-0.8.2...graphql-zod-validation-0.8.3) (2022-07-26) 17 | 18 | 19 | ### Bug Fixes 20 | 21 | * missing readme ([581e371](https://github.com/anatine/zod-plugins/commit/581e37112c223782759635ae34937a0dfa664dc9)) 22 | 23 | 24 | 25 | ## [0.8.2](https://github.com/anatine/zod-plugins/compare/graphql-zod-validation-0.8.1...graphql-zod-validation-0.8.2) (2022-07-24) 26 | 27 | 28 | ### Bug Fixes 29 | 30 | * Missing README after refactor ([00ceb10](https://github.com/anatine/zod-plugins/commit/00ceb10be8251c6be2a83e64a9a8cd6116451938)) 31 | 32 | 33 | 34 | ## [0.8.1](https://github.com/anatine/zod-plugins/compare/graphql-zod-validation-0.8.0...graphql-zod-validation-0.8.1) (2022-07-24) 35 | 36 | 37 | 38 | # [0.8.0](https://github.com/anatine/zod-plugins/compare/graphql-zod-validation-0.7.0...graphql-zod-validation-0.8.0) (2022-07-24) 39 | 40 | 41 | ### Bug Fixes 42 | 43 | * release pipeline ([bb0ad83](https://github.com/anatine/zod-plugins/commit/bb0ad836a954659b778f1181dff4fe99daf35447)), closes [PR#46](https://github.com/PR/issues/46) 44 | 45 | 46 | 47 | # [0.7.0](https://github.com/anatine/zod-plugins/compare/graphql-zod-validation-0.6.0...graphql-zod-validation-0.7.0) (2022-07-24) 48 | 49 | 50 | 51 | # [0.6.0](https://github.com/anatine/zod-plugins/compare/graphql-zod-validation-0.5.7...graphql-zod-validation-0.6.0) (2022-07-14) 52 | 53 | 54 | 55 | ## [0.0.2](https://github.com/anatine/zod-plugins/compare/graphql-zod-validation-0.0.1...graphql-zod-validation-0.0.2) (2022-07-14) 56 | 57 | 58 | 59 | ## 0.5.7 (2022-07-14) 60 | 61 | ### Bug Fixes 62 | 63 | * Adding in new release githuib actions ([29a2455](https://github.com/anatine/zod-plugins/commit/29a2455161f7021df9f933d0d8b200a08fe31fde)) 64 | * Update to new code deps ([d771c4b](https://github.com/anatine/zod-plugins/commit/d771c4b2b026635a6704eeb1fca80dd2f2e5e8e8)) 65 | -------------------------------------------------------------------------------- /packages/graphql-zod-validation/LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2022 Brian McBride 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 | -------------------------------------------------------------------------------- /packages/graphql-zod-validation/README.md: -------------------------------------------------------------------------------- 1 | # @anatine/graphql-zod-validation 2 | 3 | ## Fork for type support 4 | 5 | This codebase is taken direction from [GraphQL code generator](https://github.com/dotansimha/graphql-code-generator) 6 | 7 | Used to generate validation schema from the following libraries. 8 | 9 | - [x] support [yup](https://github.com/jquense/yup) 10 | - [x] support [zod](https://github.com/colinhacks/zod) 11 | - [x] support [myzod](https://github.com/davidmdm/myzod) 12 | 13 | ### NOTE 14 | 15 | This module has additional support for generating `type` objects from GraphQL 16 | 17 | Useful when using the expected validation type (or one extended) to validate data before returning to your resolver. 18 | 19 | ## Quick Start 20 | 21 | Start by installing this plugin and write simple plugin config; 22 | 23 | ```sh 24 | npm i @anatine/graphql-zod-validation 25 | ``` 26 | 27 | ```yml 28 | generates: 29 | path/to/graphql.ts: 30 | plugins: 31 | - typescript 32 | - '@anatine/graphql-zod-validation' # specify to use this plugin 33 | config: 34 | # You can put the config for typescript plugin here 35 | # see: https://www.graphql-code-generator.com/plugins/typescript 36 | strictScalars: true 37 | # You can also write the config for this plugin together 38 | schema: yup # or zod 39 | ``` 40 | 41 | 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. 42 | 43 | The Q&A for each schema is written in the README in the respective example directory. 44 | 45 | ## Config API Reference 46 | 47 | ### `schema` 48 | 49 | type: `ValidationSchema` default: `'yup'` 50 | 51 | Specify generete validation schema you want. 52 | 53 | You can specify `yup` or `zod` or `myzod`. 54 | 55 | ```yml 56 | generates: 57 | path/to/graphql.ts: 58 | plugins: 59 | - typescript 60 | - '@anatine/graphql-zod-validation' 61 | config: 62 | schema: yup 63 | ``` 64 | 65 | ### `importFrom` 66 | 67 | type: `string` 68 | 69 | When provided, import types from the generated typescript types file path. if not given, omit import statement. 70 | 71 | ```yml 72 | generates: 73 | path/to/graphql.ts: 74 | plugins: 75 | - typescript 76 | path/to/validation.ts: 77 | plugins: 78 | - '@anatine/graphql-zod-validation' 79 | config: 80 | importFrom: ./graphql # path for generated ts code 81 | ``` 82 | 83 | Then the generator generates code with import statement like below. 84 | 85 | ```ts 86 | import { GeneratedInput } from './graphql' 87 | /* generates validation schema here */ 88 | ``` 89 | 90 | ### `typesPrefix` 91 | 92 | type: `string` default: (empty) 93 | 94 | Prefixes all import types from generated typescript type. 95 | 96 | ```yml 97 | generates: 98 | path/to/graphql.ts: 99 | plugins: 100 | - typescript 101 | path/to/validation.ts: 102 | plugins: 103 | - '@anatine/graphql-zod-validation' 104 | config: 105 | typesPrefix: I 106 | importFrom: ./graphql # path for generated ts code 107 | ``` 108 | 109 | Then the generator generates code with import statement like below. 110 | 111 | ```ts 112 | import { IGeneratedInput } from './graphql' 113 | /* generates validation schema here */ 114 | ``` 115 | 116 | ### `typesSuffix` 117 | 118 | type: `string` default: (empty) 119 | 120 | Suffixes all import types from generated typescript type. 121 | 122 | ```yml 123 | generates: 124 | path/to/graphql.ts: 125 | plugins: 126 | - typescript 127 | path/to/validation.ts: 128 | plugins: 129 | - '@anatine/graphql-zod-validation' 130 | config: 131 | typesSuffix: I 132 | importFrom: ./graphql # path for generated ts code 133 | ``` 134 | 135 | Then the generator generates code with import statement like below. 136 | 137 | ```ts 138 | import { GeneratedInputI } from './graphql' 139 | /* generates validation schema here */ 140 | ``` 141 | 142 | ### `enumsAsTypes` 143 | 144 | type: `boolean` default: `false` 145 | 146 | Generates enum as TypeScript `type` instead of `enum`. 147 | 148 | ### `notAllowEmptyString` 149 | 150 | type: `boolean` default: `false` 151 | 152 | Generates validation string schema as do not allow empty characters by default. 153 | 154 | ### `scalarSchemas` 155 | 156 | type: `ScalarSchemas` 157 | 158 | Extends or overrides validation schema for the built-in scalars and custom GraphQL scalars. 159 | 160 | ### `useObjectTypes` 161 | 162 | type: `boolean` default: `false` 163 | 164 | When enabled, object types `type` will also be converted via codegen. 165 | 166 | #### yup schema 167 | 168 | ```yml 169 | config: 170 | schema: yup 171 | scalarSchemas: 172 | Date: yup.date() 173 | Email: yup.string().email() 174 | ``` 175 | 176 | #### zod schema 177 | 178 | ```yml 179 | config: 180 | schema: zod 181 | useObjectTypes: true 182 | scalarSchemas: 183 | Date: z.date() 184 | Email: z.string().email() 185 | ``` 186 | 187 | ### `directives` 188 | 189 | type: `DirectiveConfig` 190 | 191 | Generates validation schema with more API based on directive schema. For example, yaml config and GraphQL schema is here. 192 | 193 | ```graphql 194 | input ExampleInput { 195 | email: String! @required(msg: "Hello, World!") @constraint(minLength: 50, format: "email") 196 | message: String! @constraint(startsWith: "Hello") 197 | } 198 | ``` 199 | 200 | #### yup schema extended 201 | 202 | ```yml 203 | generates: 204 | path/to/graphql.ts: 205 | plugins: 206 | - typescript 207 | - '@anatine/graphql-zod-validation' 208 | config: 209 | schema: yup 210 | directives: 211 | # Write directives like 212 | # 213 | # directive: 214 | # arg1: schemaApi 215 | # arg2: ["schemaApi2", "Hello $1"] 216 | # 217 | # See more examples in `./tests/directive.spec.ts` 218 | # https://github.com/Code-Hex/graphql-codegen-typescript-validation-schema/blob/main/tests/directive.spec.ts 219 | required: 220 | msg: required 221 | constraint: 222 | minLength: min 223 | # Replace $1 with specified `startsWith` argument value of the constraint directive 224 | startsWith: ["matches", "/^$1/"] 225 | format: 226 | email: email 227 | ``` 228 | 229 | Then generates yup validation schema like below. 230 | 231 | ```ts 232 | export function ExampleInputSchema(): yup.SchemaOf { 233 | return yup.object({ 234 | email: yup.string().defined().required("Hello, World!").min(50).email(), 235 | message: yup.string().defined().matches(/^Hello/) 236 | }) 237 | } 238 | ``` 239 | 240 | #### zod schema extended 241 | 242 | ```yml 243 | generates: 244 | path/to/graphql.ts: 245 | plugins: 246 | - typescript 247 | - typescript-validation-schema 248 | config: 249 | schema: zod 250 | directives: 251 | # Write directives like 252 | # 253 | # directive: 254 | # arg1: schemaApi 255 | # arg2: ["schemaApi2", "Hello $1"] 256 | # 257 | # See more examples in `./tests/directive.spec.ts` 258 | # https://github.com/Code-Hex/graphql-codegen-typescript-validation-schema/blob/main/tests/directive.spec.ts 259 | constraint: 260 | minLength: min 261 | # Replace $1 with specified `startsWith` argument value of the constraint directive 262 | startsWith: ["regex", "/^$1/", "message"] 263 | format: 264 | email: email 265 | ``` 266 | 267 | Then generates zod validation schema like below. 268 | 269 | ```ts 270 | export function ExampleInputSchema(): z.ZodSchema { 271 | return z.object({ 272 | email: z.string().min(50).email(), 273 | message: z.string().regex(/^Hello/, "message") 274 | }) 275 | } 276 | ``` 277 | 278 | #### other schema 279 | 280 | Please see [example](https://github.com/Code-Hex/graphql-codegen-typescript-validation-schema/tree/main/example) directory. 281 | -------------------------------------------------------------------------------- /packages/graphql-zod-validation/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'graphql-zod-validation', 4 | preset: '../../jest.preset.js', 5 | globals: {}, 6 | testEnvironment: 'node', 7 | transform: { 8 | '^.+\\.[tj]sx?$': [ 9 | 'ts-jest', 10 | { 11 | tsconfig: '/tsconfig.spec.json', 12 | }, 13 | ], 14 | }, 15 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 16 | coverageDirectory: '../../coverage/packages/graphql-zod-validation', 17 | }; 18 | -------------------------------------------------------------------------------- /packages/graphql-zod-validation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@anatine/graphql-zod-validation", 3 | "version": "0.9.1", 4 | "description": "GraphQL Code Generator plugin to generate form validation schema from your GraphQL schema", 5 | "keywords": [ 6 | "gql", 7 | "generator", 8 | "yup", 9 | "zod", 10 | "code", 11 | "types", 12 | "graphql", 13 | "codegen", 14 | "apollo", 15 | "node", 16 | "types", 17 | "typings" 18 | ], 19 | "license": "MIT", 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/anatine/zod-plugins" 23 | }, 24 | "homepage": "https://github.com/anatine/zod-plugins/tree/main/packages/graphql-zod-validation", 25 | "author": { 26 | "name": "codehex" 27 | }, 28 | "contributors": [ 29 | { 30 | "name": "Brian McBride", 31 | "url": "https://www.linkedin.com/in/brianmcbride" 32 | } 33 | ], 34 | "dependencies": { 35 | "@graphql-codegen/plugin-helpers": "^2.6.0", 36 | "@graphql-codegen/schema-ast": "^2.5.0", 37 | "@graphql-codegen/visitor-plugin-common": "^2.12.0", 38 | "@graphql-tools/utils": "^8.8.0" 39 | }, 40 | "peerDependencies": { 41 | "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/graphql-zod-validation/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-zod-validation", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "packages/graphql-zod-validation/src", 5 | "projectType": "library", 6 | "targets": { 7 | "build": { 8 | "executor": "@nx/js:tsc", 9 | "outputs": ["{options.outputPath}"], 10 | "options": { 11 | "outputPath": "dist/packages/graphql-zod-validation", 12 | "tsConfig": "packages/graphql-zod-validation/tsconfig.lib.json", 13 | "packageJson": "packages/graphql-zod-validation/package.json", 14 | "main": "packages/graphql-zod-validation/src/index.ts", 15 | "assets": ["packages/graphql-zod-validation/*.md"] 16 | } 17 | }, 18 | "lint": { 19 | "executor": "@nx/linter:eslint", 20 | "outputs": ["{options.outputFile}"], 21 | "options": { 22 | "lintFilePatterns": ["packages/graphql-zod-validation/**/*.ts"] 23 | } 24 | }, 25 | "test": { 26 | "executor": "@nx/jest:jest", 27 | "outputs": ["{workspaceRoot}/coverage/packages/graphql-zod-validation"], 28 | "options": { 29 | "jestConfig": "packages/graphql-zod-validation/jest.config.ts", 30 | "passWithNoTests": true 31 | } 32 | }, 33 | "version": { 34 | "executor": "@jscutlery/semver:version", 35 | "options": { 36 | "push": true, 37 | "preset": "conventional", 38 | "skipCommitTypes": ["ci"], 39 | "postTargets": [ 40 | "graphql-zod-validation:build", 41 | "graphql-zod-validation:publish", 42 | "graphql-zod-validation:github" 43 | ] 44 | } 45 | }, 46 | "github": { 47 | "executor": "@jscutlery/semver:github", 48 | "options": { 49 | "tag": "${tag}", 50 | "notes": "${notes}" 51 | } 52 | }, 53 | "publish": { 54 | "executor": "ngx-deploy-npm:deploy", 55 | "options": { 56 | "access": "public", 57 | "distFolderPath": "dist/packages/graphql-zod-validation" 58 | }, 59 | "dependsOn": ["build"] 60 | } 61 | }, 62 | "tags": [] 63 | } 64 | -------------------------------------------------------------------------------- /packages/graphql-zod-validation/src/config.ts: -------------------------------------------------------------------------------- 1 | import { TypeScriptPluginConfig } from '@graphql-codegen/typescript'; 2 | 3 | export type ValidationSchema = 'yup' | 'zod' | 'myzod'; 4 | 5 | export interface DirectiveConfig { 6 | [directive: string]: { 7 | [argument: string]: string | string[] | DirectiveObjectArguments; 8 | }; 9 | } 10 | 11 | export interface DirectiveObjectArguments { 12 | [matched: string]: string | string[]; 13 | } 14 | 15 | interface ScalarSchemas { 16 | [name: string]: string; 17 | } 18 | 19 | export interface ValidationSchemaPluginConfig extends TypeScriptPluginConfig { 20 | /** 21 | * @description specify generate schema 22 | * @default yup 23 | * 24 | * @exampleMarkdown 25 | * ```yml 26 | * generates: 27 | * path/to/file.ts: 28 | * plugins: 29 | * - typescript 30 | * - graphql-codegen-validation-schema 31 | * config: 32 | * schema: yup 33 | * ``` 34 | */ 35 | schema?: ValidationSchema; 36 | /** 37 | * @description import types from generated typescript type path 38 | * if not given, omit import statement. 39 | * 40 | * @exampleMarkdown 41 | * ```yml 42 | * generates: 43 | * path/to/types.ts: 44 | * plugins: 45 | * - typescript 46 | * path/to/schemas.ts: 47 | * plugins: 48 | * - graphql-codegen-validation-schema 49 | * config: 50 | * schema: yup 51 | * importFrom: ./path/to/types 52 | * ``` 53 | */ 54 | importFrom?: string; 55 | /** 56 | * @description Prefixes all import types from generated typescript type. 57 | * @default "" 58 | * 59 | * @exampleMarkdown 60 | * ```yml 61 | * generates: 62 | * path/to/types.ts: 63 | * plugins: 64 | * - typescript 65 | * path/to/schemas.ts: 66 | * plugins: 67 | * - graphql-codegen-validation-schema 68 | * config: 69 | * typesPrefix: I 70 | * importFrom: ./path/to/types 71 | * ``` 72 | */ 73 | typesPrefix?: string; 74 | /** 75 | * @description Suffixes all import types from generated typescript type. 76 | * @default "" 77 | * 78 | * @exampleMarkdown 79 | * ```yml 80 | * generates: 81 | * path/to/types.ts: 82 | * plugins: 83 | * - typescript 84 | * path/to/schemas.ts: 85 | * plugins: 86 | * - graphql-codegen-validation-schema 87 | * config: 88 | * typesSuffix: I 89 | * importFrom: ./path/to/types 90 | * ``` 91 | */ 92 | typesSuffix?: string; 93 | /** 94 | * @description Generates validation schema for enum as TypeScript `type` 95 | * @default false 96 | * 97 | * @exampleMarkdown 98 | * ```yml 99 | * generates: 100 | * path/to/file.ts: 101 | * plugins: 102 | * - graphql-codegen-validation-schema 103 | * config: 104 | * enumsAsTypes: true 105 | * ``` 106 | * 107 | * ```yml 108 | * generates: 109 | * path/to/file.ts: 110 | * plugins: 111 | * - typescript 112 | * - graphql-codegen-validation-schema 113 | * config: 114 | * enumsAsTypes: true 115 | * ``` 116 | */ 117 | enumsAsTypes?: boolean; 118 | /** 119 | * @description Generates validation string schema as do not allow empty characters by default. 120 | * @default false 121 | * 122 | * @exampleMarkdown 123 | * ```yml 124 | * generates: 125 | * path/to/file.ts: 126 | * plugins: 127 | * - graphql-codegen-validation-schema 128 | * config: 129 | * notAllowEmptyString: true 130 | * ``` 131 | */ 132 | notAllowEmptyString?: boolean; 133 | /** 134 | * @description Extends or overrides validation schema for the built-in scalars and custom GraphQL scalars. 135 | * 136 | * @exampleMarkdown 137 | * ```yml 138 | * config: 139 | * schema: yup 140 | * scalarSchemas: 141 | * Date: yup.date() 142 | * Email: yup.string().email() 143 | * ``` 144 | * 145 | * @exampleMarkdown 146 | * ```yml 147 | * config: 148 | * schema: zod 149 | * scalarSchemas: 150 | * Date: z.date() 151 | * Email: z.string().email() 152 | * ``` 153 | */ 154 | scalarSchemas?: ScalarSchemas; 155 | /** 156 | * @description Generates validation schema with more API based on directive schema. 157 | * @exampleMarkdown 158 | * ```yml 159 | * generates: 160 | * path/to/file.ts: 161 | * plugins: 162 | * - graphql-codegen-validation-schema 163 | * config: 164 | * schema: yup 165 | * directives: 166 | * required: 167 | * msg: required 168 | * # This is example using constraint directive. 169 | * # see: https://github.com/confuser/graphql-constraint-directive 170 | * constraint: 171 | * minLength: min # same as ['min', '$1'] 172 | * maxLength: max 173 | * startsWith: ["matches", "/^$1/"] 174 | * endsWith: ["matches", "/$1$/"] 175 | * contains: ["matches", "/$1/"] 176 | * notContains: ["matches", "/^((?!$1).)*$/"] 177 | * pattern: ["matches", "/$1/"] 178 | * format: 179 | * # For example, `@constraint(format: "uri")`. this case $1 will be "uri". 180 | * # Therefore the generator generates yup schema `.url()` followed by `uri: 'url'` 181 | * # If $1 does not match anywhere, the generator will ignore. 182 | * uri: url 183 | * email: email 184 | * uuid: uuid 185 | * # yup does not have `ipv4` API. If you want to add this, 186 | * # you need to add the logic using `yup.addMethod`. 187 | * # see: https://github.com/jquense/yup#addmethodschematype-schema-name-string-method--schema-void 188 | * ipv4: ipv4 189 | * min: ["min", "$1 - 1"] 190 | * max: ["max", "$1 + 1"] 191 | * exclusiveMin: min 192 | * exclusiveMax: max 193 | * ``` 194 | */ 195 | directives?: DirectiveConfig; 196 | /** 197 | * @description Converts the regular graphql type into a zod validation function. 198 | * 199 | * @exampleMarkdown 200 | * ```yml 201 | * generates: 202 | * path/to/types.ts: 203 | * plugins: 204 | * - typescript 205 | * path/to/schemas.ts: 206 | * plugins: 207 | * - graphql-codegen-validation-schema 208 | * config: 209 | * schema: yup 210 | * useObjectTypes: true 211 | * ``` 212 | */ 213 | useObjectTypes?: boolean; 214 | } 215 | -------------------------------------------------------------------------------- /packages/graphql-zod-validation/src/directive.ts: -------------------------------------------------------------------------------- 1 | import { ConstArgumentNode, ConstDirectiveNode, ConstValueNode, Kind, valueFromASTUntyped } from 'graphql'; 2 | import { DirectiveConfig, DirectiveObjectArguments } from './config'; 3 | import { isConvertableRegexp } from './regexp'; 4 | 5 | export interface FormattedDirectiveConfig { 6 | [directive: string]: FormattedDirectiveArguments; 7 | } 8 | 9 | export interface FormattedDirectiveArguments { 10 | [argument: string]: string[] | FormattedDirectiveObjectArguments | undefined; 11 | } 12 | 13 | export interface FormattedDirectiveObjectArguments { 14 | [matched: string]: string[] | undefined; 15 | } 16 | 17 | const isFormattedDirectiveObjectArguments = ( 18 | arg: FormattedDirectiveArguments[keyof FormattedDirectiveArguments] 19 | ): arg is FormattedDirectiveObjectArguments => arg !== undefined && !Array.isArray(arg); 20 | 21 | // ```yml 22 | // directives: 23 | // required: 24 | // msg: required 25 | // constraint: 26 | // minLength: min 27 | // format: 28 | // uri: url 29 | // email: email 30 | // ``` 31 | // 32 | // This function convterts to like below 33 | // { 34 | // 'required': { 35 | // 'msg': ['required', '$1'], 36 | // }, 37 | // 'constraint': { 38 | // 'minLength': ['min', '$1'], 39 | // 'format': { 40 | // 'uri': ['url', '$2'], 41 | // 'email': ['email', '$2'], 42 | // } 43 | // } 44 | // } 45 | export const formatDirectiveConfig = (config: DirectiveConfig): FormattedDirectiveConfig => { 46 | return Object.fromEntries( 47 | Object.entries(config).map(([directive, arg]) => { 48 | const formatted = Object.fromEntries( 49 | Object.entries(arg).map(([arg, val]) => { 50 | if (Array.isArray(val)) { 51 | return [arg, val]; 52 | } 53 | if (typeof val === 'string') { 54 | return [arg, [val, '$1']]; 55 | } 56 | return [arg, formatDirectiveObjectArguments(val)]; 57 | }) 58 | ); 59 | return [directive, formatted]; 60 | }) 61 | ); 62 | }; 63 | 64 | // ```yml 65 | // format: 66 | // # For example, `@constraint(format: "uri")`. this case $1 will be "uri". 67 | // # Therefore the generator generates yup schema `.url()` followed by `uri: 'url'` 68 | // # If $1 does not match anywhere, the generator will ignore. 69 | // uri: url 70 | // email: ["email", "$2"] 71 | // ``` 72 | // 73 | // This function convterts to like below 74 | // { 75 | // 'uri': ['url', '$2'], 76 | // 'email': ['email'], 77 | // } 78 | export const formatDirectiveObjectArguments = (args: DirectiveObjectArguments): FormattedDirectiveObjectArguments => { 79 | const formatted = Object.entries(args).map(([arg, val]) => { 80 | if (Array.isArray(val)) { 81 | return [arg, val]; 82 | } 83 | return [arg, [val, '$2']]; 84 | }); 85 | return Object.fromEntries(formatted); 86 | }; 87 | 88 | // This function generates `.required("message").min(100).email()` 89 | // 90 | // config 91 | // { 92 | // 'required': { 93 | // 'msg': ['required', '$1'], 94 | // }, 95 | // 'constraint': { 96 | // 'minLength': ['min', '$1'], 97 | // 'format': { 98 | // 'uri': ['url', '$2'], 99 | // 'email': ['email', '$2'], 100 | // } 101 | // } 102 | // } 103 | // 104 | // GraphQL schema 105 | // ```graphql 106 | // input ExampleInput { 107 | // email: String! @required(msg: "message") @constraint(minLength: 100, format: "email") 108 | // } 109 | // ``` 110 | export const buildApi = (config: FormattedDirectiveConfig, directives: ReadonlyArray): string => 111 | directives 112 | .filter(directive => config[directive.name.value] !== undefined) 113 | .map(directive => { 114 | const directiveName = directive.name.value; 115 | const argsConfig = config[directiveName]; 116 | return buildApiFromDirectiveArguments(argsConfig, directive.arguments ?? []); 117 | }) 118 | .join(''); 119 | 120 | const buildApiSchema = (validationSchema: string[] | undefined, argValue: ConstValueNode): string => { 121 | if (!validationSchema) { 122 | return ''; 123 | } 124 | const schemaApi = validationSchema[0]; 125 | const schemaApiArgs = validationSchema.slice(1).map(templateArg => { 126 | const gqlSchemaArgs = apiArgsFromConstValueNode(argValue); 127 | return applyArgToApiSchemaTemplate(templateArg, gqlSchemaArgs); 128 | }); 129 | return `.${schemaApi}(${schemaApiArgs.join(', ')})`; 130 | }; 131 | 132 | const buildApiFromDirectiveArguments = ( 133 | config: FormattedDirectiveArguments, 134 | args: ReadonlyArray 135 | ): string => { 136 | return args 137 | .map(arg => { 138 | const argName = arg.name.value; 139 | const validationSchema = config[argName]; 140 | if (isFormattedDirectiveObjectArguments(validationSchema)) { 141 | return buildApiFromDirectiveObjectArguments(validationSchema, arg.value); 142 | } 143 | return buildApiSchema(validationSchema, arg.value); 144 | }) 145 | .join(''); 146 | }; 147 | 148 | const buildApiFromDirectiveObjectArguments = ( 149 | config: FormattedDirectiveObjectArguments, 150 | argValue: ConstValueNode 151 | ): string => { 152 | if (argValue.kind !== Kind.STRING) { 153 | return ''; 154 | } 155 | const validationSchema = config[argValue.value]; 156 | return buildApiSchema(validationSchema, argValue); 157 | }; 158 | 159 | const applyArgToApiSchemaTemplate = (template: string, apiArgs: any[]): string => { 160 | const matches = template.matchAll(/[$](\d+)/g); 161 | for (const match of matches) { 162 | const placeholder = match[0]; // `$1` 163 | const idx = parseInt(match[1], 10) - 1; // start with `1 - 1` 164 | const apiArg = apiArgs[idx]; 165 | if (!apiArg) { 166 | template = template.replace(placeholder, ''); 167 | continue; 168 | } 169 | if (template === placeholder) { 170 | return stringify(apiArg); 171 | } 172 | template = template.replace(placeholder, apiArg); 173 | } 174 | if (template !== '') { 175 | return stringify(template, true); 176 | } 177 | return template; 178 | }; 179 | 180 | const stringify = (arg: any, quoteString?: boolean): string => { 181 | if (Array.isArray(arg)) { 182 | return arg.map(v => stringify(v, true)).join(','); 183 | } 184 | if (typeof arg === 'string') { 185 | if (isConvertableRegexp(arg)) { 186 | return arg; 187 | } 188 | if (quoteString) { 189 | return JSON.stringify(arg); 190 | } 191 | } 192 | if (typeof arg === 'boolean' || typeof arg === 'number' || typeof arg === 'bigint') { 193 | return `${arg}`; 194 | } 195 | return JSON.stringify(arg); 196 | }; 197 | 198 | const apiArgsFromConstValueNode = (value: ConstValueNode): any[] => { 199 | const val = valueFromASTUntyped(value); 200 | if (Array.isArray(val)) { 201 | return val; 202 | } 203 | return [val]; 204 | }; 205 | 206 | export const exportedForTesting = { 207 | applyArgToApiSchemaTemplate, 208 | buildApiFromDirectiveObjectArguments, 209 | buildApiFromDirectiveArguments, 210 | }; 211 | -------------------------------------------------------------------------------- /packages/graphql-zod-validation/src/graphql.ts: -------------------------------------------------------------------------------- 1 | import { ListTypeNode, NonNullTypeNode, NamedTypeNode, TypeNode } from 'graphql'; 2 | 3 | export const isListType = (typ?: TypeNode): typ is ListTypeNode => typ?.kind === 'ListType'; 4 | export const isNonNullType = (typ?: TypeNode): typ is NonNullTypeNode => typ?.kind === 'NonNullType'; 5 | export const isNamedType = (typ?: TypeNode): typ is NamedTypeNode => typ?.kind === 'NamedType'; 6 | 7 | export const isInput = (kind: string) => kind.includes('Input'); 8 | -------------------------------------------------------------------------------- /packages/graphql-zod-validation/src/index.ts: -------------------------------------------------------------------------------- 1 | import { ZodSchemaVisitor } from './zod/index'; 2 | import { MyZodSchemaVisitor } from './myzod/index'; 3 | import { transformSchemaAST } from '@graphql-codegen/schema-ast'; 4 | import { YupSchemaVisitor } from './yup/index'; 5 | import { ValidationSchemaPluginConfig } from './config'; 6 | import { PluginFunction, Types } from '@graphql-codegen/plugin-helpers'; 7 | import { GraphQLSchema, visit } from 'graphql'; 8 | 9 | export const plugin: PluginFunction< 10 | ValidationSchemaPluginConfig, 11 | Types.ComplexPluginOutput 12 | > = ( 13 | schema: GraphQLSchema, 14 | _documents: Types.DocumentFile[], 15 | config: ValidationSchemaPluginConfig 16 | ): Types.ComplexPluginOutput => { 17 | const { schema: _schema, ast } = transformSchemaAST(schema, config); 18 | const { buildImports, initialEmit, ...visitor } = schemaVisitor( 19 | _schema, 20 | config 21 | ); 22 | 23 | const result = visit(ast, { 24 | InputObjectTypeDefinition: { 25 | leave: visitor.InputObjectTypeDefinition, 26 | }, 27 | ObjectTypeDefinition: { 28 | leave: visitor.ObjectTypeDefinition, 29 | }, 30 | EnumTypeDefinition: { 31 | leave: visitor.EnumTypeDefinition, 32 | }, 33 | }); 34 | 35 | const generated = result.definitions.filter((def) => typeof def === 'string'); 36 | 37 | return { 38 | prepend: buildImports(), 39 | content: [initialEmit(), ...generated].join('\n'), 40 | }; 41 | }; 42 | 43 | const schemaVisitor = ( 44 | schema: GraphQLSchema, 45 | config: ValidationSchemaPluginConfig 46 | ) => { 47 | if (config?.schema === 'zod') { 48 | return ZodSchemaVisitor(schema, config); 49 | } else if (config?.schema === 'myzod') { 50 | return MyZodSchemaVisitor(schema, config); 51 | } 52 | return YupSchemaVisitor(schema, config); 53 | }; 54 | -------------------------------------------------------------------------------- /packages/graphql-zod-validation/src/myzod/index.ts: -------------------------------------------------------------------------------- 1 | import { isInput, isNonNullType, isListType, isNamedType } from './../graphql'; 2 | import { ValidationSchemaPluginConfig } from '../config'; 3 | import { 4 | InputValueDefinitionNode, 5 | NameNode, 6 | TypeNode, 7 | GraphQLSchema, 8 | InputObjectTypeDefinitionNode, 9 | ObjectTypeDefinitionNode, 10 | EnumTypeDefinitionNode, 11 | FieldDefinitionNode, 12 | } from 'graphql'; 13 | import { 14 | DeclarationBlock, 15 | indent, 16 | } from '@graphql-codegen/visitor-plugin-common'; 17 | import { TsVisitor } from '@graphql-codegen/typescript'; 18 | import { buildApi, formatDirectiveConfig } from '../directive'; 19 | 20 | const importZod = `import * as myzod from 'myzod'`; 21 | const anySchema = `definedNonNullAnySchema`; 22 | 23 | export const MyZodSchemaVisitor = ( 24 | schema: GraphQLSchema, 25 | config: ValidationSchemaPluginConfig 26 | ) => { 27 | const tsVisitor = new TsVisitor(schema, config); 28 | 29 | const importTypes: string[] = []; 30 | 31 | return { 32 | buildImports: (): string[] => { 33 | if (config.importFrom && importTypes.length > 0) { 34 | return [ 35 | importZod, 36 | `import { ${importTypes.join(', ')} } from '${config.importFrom}'`, 37 | ]; 38 | } 39 | return [importZod]; 40 | }, 41 | initialEmit: (): string => 42 | '\n' + 43 | [ 44 | new DeclarationBlock({}) 45 | .export() 46 | .asKind('const') 47 | .withName(`${anySchema}`) 48 | .withContent(`myzod.object({})`).string, 49 | ].join('\n'), 50 | InputObjectTypeDefinition: (node: InputObjectTypeDefinitionNode) => { 51 | const name = tsVisitor.convertName(node.name.value); 52 | importTypes.push(name); 53 | 54 | const shape = node.fields 55 | ?.map((field) => 56 | generateFieldMyZodSchema(config, tsVisitor, schema, field, 2) 57 | ) 58 | .join(',\n'); 59 | 60 | return new DeclarationBlock({}) 61 | .export() 62 | .asKind('function') 63 | .withName(`${name}Schema(): myzod.Type<${name}>`) 64 | .withBlock( 65 | [indent(`return myzod.object({`), shape, indent('})')].join('\n') 66 | ).string; 67 | }, 68 | ObjectTypeDefinition: (node: ObjectTypeDefinitionNode) => { 69 | if (!config.useObjectTypes) return; 70 | if (node.name.value.toLowerCase() === 'query') return; 71 | if (node.name.value.toLowerCase() === 'mutation') return; 72 | const name = tsVisitor.convertName(node.name.value); 73 | importTypes.push(name); 74 | 75 | const shape = node.fields 76 | ?.map((field) => 77 | generateFieldMyZodSchema(config, tsVisitor, schema, field, 2) 78 | ) 79 | .join(',\n'); 80 | 81 | return new DeclarationBlock({}) 82 | .export() 83 | .asKind('function') 84 | .withName(`${name}Schema(): myzod.Type<${name}>`) 85 | .withBlock( 86 | [ 87 | indent(`return myzod.object({`), 88 | ` __typename: myzod.literal('${node.name.value}').optional(),\n${shape}`, 89 | indent('})'), 90 | ].join('\n') 91 | ).string; 92 | }, 93 | EnumTypeDefinition: (node: EnumTypeDefinitionNode) => { 94 | const enumname = tsVisitor.convertName(node.name.value); 95 | importTypes.push(enumname); 96 | // z.enum are basically myzod.literals 97 | if (config.enumsAsTypes) { 98 | return new DeclarationBlock({}) 99 | .export() 100 | .asKind('type') 101 | .withName(`${enumname}Schema`) 102 | .withContent( 103 | `myzod.literals(${node.values 104 | ?.map((enumOption) => `'${enumOption.name.value}'`) 105 | .join(', ')})` 106 | ).string; 107 | } 108 | 109 | return new DeclarationBlock({}) 110 | .export() 111 | .asKind('const') 112 | .withName(`${enumname}Schema`) 113 | .withContent(`myzod.enum(${enumname})`).string; 114 | }, 115 | }; 116 | }; 117 | 118 | const generateFieldMyZodSchema = ( 119 | config: ValidationSchemaPluginConfig, 120 | tsVisitor: TsVisitor, 121 | schema: GraphQLSchema, 122 | field: InputValueDefinitionNode | FieldDefinitionNode, 123 | indentCount: number 124 | ): string => { 125 | const gen = generateFieldTypeMyZodSchema( 126 | config, 127 | tsVisitor, 128 | schema, 129 | field, 130 | field.type 131 | ); 132 | return indent( 133 | `${field.name.value}: ${maybeLazy(field.type, gen)}`, 134 | indentCount 135 | ); 136 | }; 137 | 138 | const generateFieldTypeMyZodSchema = ( 139 | config: ValidationSchemaPluginConfig, 140 | tsVisitor: TsVisitor, 141 | schema: GraphQLSchema, 142 | field: InputValueDefinitionNode | FieldDefinitionNode, 143 | type: TypeNode, 144 | parentType?: TypeNode 145 | ): string => { 146 | if (isListType(type)) { 147 | const gen = generateFieldTypeMyZodSchema( 148 | config, 149 | tsVisitor, 150 | schema, 151 | field, 152 | type.type, 153 | type 154 | ); 155 | if (!isNonNullType(parentType)) { 156 | const arrayGen = `myzod.array(${maybeLazy(type.type, gen)})`; 157 | const maybeLazyGen = applyDirectives(config, field, arrayGen); 158 | return `${maybeLazyGen}.optional().nullable()`; 159 | } 160 | return `myzod.array(${maybeLazy(type.type, gen)})`; 161 | } 162 | if (isNonNullType(type)) { 163 | const gen = generateFieldTypeMyZodSchema( 164 | config, 165 | tsVisitor, 166 | schema, 167 | field, 168 | type.type, 169 | type 170 | ); 171 | return maybeLazy(type.type, gen); 172 | } 173 | if (isNamedType(type)) { 174 | const gen = generateNameNodeMyZodSchema( 175 | config, 176 | tsVisitor, 177 | schema, 178 | type.name 179 | ); 180 | if (isListType(parentType)) { 181 | return `${gen}.nullable()`; 182 | } 183 | const appliedDirectivesGen = applyDirectives(config, field, gen); 184 | if (isNonNullType(parentType)) { 185 | if (config.notAllowEmptyString === true) { 186 | const tsType = tsVisitor.scalars[type.name.value]; 187 | if (tsType === 'string') return `${gen}.min(1)`; 188 | } 189 | return appliedDirectivesGen; 190 | } 191 | if (isListType(parentType)) { 192 | return `${appliedDirectivesGen}.nullable()`; 193 | } 194 | return `${appliedDirectivesGen}.optional().nullable()`; 195 | } 196 | console.warn('unhandled type:', type); 197 | return ''; 198 | }; 199 | 200 | const applyDirectives = ( 201 | config: ValidationSchemaPluginConfig, 202 | field: InputValueDefinitionNode | FieldDefinitionNode, 203 | gen: string 204 | ): string => { 205 | if (config.directives && field.directives) { 206 | const formatted = formatDirectiveConfig(config.directives); 207 | return gen + buildApi(formatted, field.directives); 208 | } 209 | return gen; 210 | }; 211 | 212 | const generateNameNodeMyZodSchema = ( 213 | config: ValidationSchemaPluginConfig, 214 | tsVisitor: TsVisitor, 215 | schema: GraphQLSchema, 216 | node: NameNode 217 | ): string => { 218 | const typ = schema.getType(node.value); 219 | 220 | if ( 221 | typ && 222 | (typ.astNode?.kind === 'InputObjectTypeDefinition' || 223 | typ.astNode?.kind === 'ObjectTypeDefinition') 224 | ) { 225 | const enumName = tsVisitor.convertName(typ.astNode.name.value); 226 | return `${enumName}Schema()`; 227 | } 228 | 229 | if (typ && typ.astNode?.kind === 'EnumTypeDefinition') { 230 | const enumName = tsVisitor.convertName(typ.astNode.name.value); 231 | return `${enumName}Schema`; 232 | } 233 | 234 | return myzod4Scalar(config, tsVisitor, node.value); 235 | }; 236 | 237 | const maybeLazy = (type: TypeNode, schema: string): string => { 238 | if (isNamedType(type) && isInput(type.name.value)) { 239 | return `myzod.lazy(() => ${schema})`; 240 | } 241 | return schema; 242 | }; 243 | 244 | const myzod4Scalar = ( 245 | config: ValidationSchemaPluginConfig, 246 | tsVisitor: TsVisitor, 247 | scalarName: string 248 | ): string => { 249 | if (config.scalarSchemas?.[scalarName]) { 250 | return config.scalarSchemas[scalarName]; 251 | } 252 | const tsType = tsVisitor.scalars[scalarName]; 253 | switch (tsType) { 254 | case 'string': 255 | return `myzod.string()`; 256 | case 'number': 257 | return `myzod.number()`; 258 | case 'boolean': 259 | return `myzod.boolean()`; 260 | } 261 | console.warn('unhandled name:', scalarName); 262 | return anySchema; 263 | }; 264 | -------------------------------------------------------------------------------- /packages/graphql-zod-validation/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 | -------------------------------------------------------------------------------- /packages/graphql-zod-validation/src/yup/index.ts: -------------------------------------------------------------------------------- 1 | import { isInput, isNonNullType, isListType, isNamedType } from './../graphql'; 2 | import { ValidationSchemaPluginConfig } from '../config'; 3 | import { 4 | InputValueDefinitionNode, 5 | NameNode, 6 | TypeNode, 7 | GraphQLSchema, 8 | InputObjectTypeDefinitionNode, 9 | EnumTypeDefinitionNode, 10 | ObjectTypeDefinitionNode, 11 | FieldDefinitionNode, 12 | } from 'graphql'; 13 | import { 14 | DeclarationBlock, 15 | indent, 16 | } from '@graphql-codegen/visitor-plugin-common'; 17 | import { TsVisitor } from '@graphql-codegen/typescript'; 18 | import { buildApi, formatDirectiveConfig } from '../directive'; 19 | 20 | const importYup = `import * as yup from 'yup'`; 21 | 22 | export const YupSchemaVisitor = ( 23 | schema: GraphQLSchema, 24 | config: ValidationSchemaPluginConfig 25 | ) => { 26 | const tsVisitor = new TsVisitor(schema, config); 27 | 28 | const importTypes: string[] = []; 29 | 30 | return { 31 | buildImports: (): string[] => { 32 | if (config.importFrom && importTypes.length > 0) { 33 | return [ 34 | importYup, 35 | `import { ${importTypes.join(', ')} } from '${config.importFrom}'`, 36 | ]; 37 | } 38 | return [importYup]; 39 | }, 40 | initialEmit: (): string => '', 41 | InputObjectTypeDefinition: (node: InputObjectTypeDefinitionNode) => { 42 | const name = tsVisitor.convertName(node.name.value); 43 | importTypes.push(name); 44 | 45 | const shape = node.fields 46 | ?.map((field) => 47 | generateFieldYupSchema(config, tsVisitor, schema, field, 2) 48 | ) 49 | .join(',\n'); 50 | 51 | return new DeclarationBlock({}) 52 | .export() 53 | .asKind('function') 54 | .withName(`${name}Schema(): yup.SchemaOf<${name}>`) 55 | .withBlock( 56 | [indent(`return yup.object({`), shape, indent('})')].join('\n') 57 | ).string; 58 | }, 59 | ObjectTypeDefinition: (node: ObjectTypeDefinitionNode) => { 60 | if (!config.useObjectTypes) return; 61 | const name = tsVisitor.convertName(node.name.value); 62 | importTypes.push(name); 63 | 64 | const shape = node.fields 65 | ?.map((field) => 66 | generateFieldYupSchema(config, tsVisitor, schema, field, 2) 67 | ) 68 | .join(',\n'); 69 | 70 | return new DeclarationBlock({}) 71 | .export() 72 | .asKind('function') 73 | .withName(`${name}Schema(): yup.SchemaOf<${name}>`) 74 | .withBlock( 75 | [ 76 | indent(`return yup.object({`), 77 | ` __typename: yup.mixed().oneOf(['${node.name.value}', undefined]),\n${shape}`, 78 | indent('})'), 79 | ].join('\n') 80 | ).string; 81 | }, 82 | EnumTypeDefinition: (node: EnumTypeDefinitionNode) => { 83 | const enumname = tsVisitor.convertName(node.name.value); 84 | importTypes.push(enumname); 85 | 86 | if (config.enumsAsTypes) { 87 | return new DeclarationBlock({}) 88 | .export() 89 | .asKind('const') 90 | .withName(`${enumname}Schema`) 91 | .withContent( 92 | `yup.mixed().oneOf([${node.values 93 | ?.map((enumOption) => `'${enumOption.name.value}'`) 94 | .join(', ')}])` 95 | ).string; 96 | } 97 | 98 | const values = node.values 99 | ?.map( 100 | (enumOption) => 101 | `${enumname}.${tsVisitor.convertName(enumOption.name, { 102 | useTypesPrefix: false, 103 | transformUnderscore: true, 104 | })}` 105 | ) 106 | .join(', '); 107 | return new DeclarationBlock({}) 108 | .export() 109 | .asKind('const') 110 | .withName(`${enumname}Schema`) 111 | .withContent(`yup.mixed().oneOf([${values}])`).string; 112 | }, 113 | // ScalarTypeDefinition: (node) => { 114 | // const decl = new DeclarationBlock({}) 115 | // .export() 116 | // .asKind("const") 117 | // .withName(`${node.name.value}Schema`); 118 | 119 | // if (tsVisitor.scalars[node.name.value]) { 120 | // const tsType = tsVisitor.scalars[node.name.value]; 121 | // switch (tsType) { 122 | // case "string": 123 | // return decl.withContent(`yup.string()`).string; 124 | // case "number": 125 | // return decl.withContent(`yup.number()`).string; 126 | // case "boolean": 127 | // return decl.withContent(`yup.boolean()`).string; 128 | // } 129 | // } 130 | // return decl.withContent(`yup.mixed()`).string; 131 | // }, 132 | }; 133 | }; 134 | 135 | const generateFieldYupSchema = ( 136 | config: ValidationSchemaPluginConfig, 137 | tsVisitor: TsVisitor, 138 | schema: GraphQLSchema, 139 | field: InputValueDefinitionNode | FieldDefinitionNode, 140 | indentCount: number 141 | ): string => { 142 | let gen = generateFieldTypeYupSchema(config, tsVisitor, schema, field.type); 143 | if (config.directives && field.directives) { 144 | const formatted = formatDirectiveConfig(config.directives); 145 | gen += buildApi(formatted, field.directives); 146 | } 147 | return indent( 148 | `${field.name.value}: ${maybeLazy(field.type, gen)}`, 149 | indentCount 150 | ); 151 | }; 152 | 153 | const generateFieldTypeYupSchema = ( 154 | config: ValidationSchemaPluginConfig, 155 | tsVisitor: TsVisitor, 156 | schema: GraphQLSchema, 157 | type: TypeNode, 158 | parentType?: TypeNode 159 | ): string => { 160 | if (isListType(type)) { 161 | const gen = generateFieldTypeYupSchema( 162 | config, 163 | tsVisitor, 164 | schema, 165 | type.type, 166 | type 167 | ); 168 | if (!isNonNullType(parentType)) { 169 | return `yup.array().of(${maybeLazy(type.type, gen)}).optional()`; 170 | } 171 | return `yup.array().of(${maybeLazy(type.type, gen)})`; 172 | } 173 | if (isNonNullType(type)) { 174 | const gen = generateFieldTypeYupSchema( 175 | config, 176 | tsVisitor, 177 | schema, 178 | type.type, 179 | type 180 | ); 181 | const nonNullGen = maybeNonEmptyString(config, tsVisitor, gen, type.type); 182 | return maybeLazy(type.type, nonNullGen); 183 | } 184 | if (isNamedType(type)) { 185 | return generateNameNodeYupSchema(config, tsVisitor, schema, type.name); 186 | } 187 | console.warn('unhandled type:', type); 188 | return ''; 189 | }; 190 | 191 | const generateNameNodeYupSchema = ( 192 | config: ValidationSchemaPluginConfig, 193 | tsVisitor: TsVisitor, 194 | schema: GraphQLSchema, 195 | node: NameNode 196 | ): string => { 197 | const typ = schema.getType(node.value); 198 | 199 | if (typ && typ.astNode?.kind === 'InputObjectTypeDefinition') { 200 | const enumName = tsVisitor.convertName(typ.astNode.name.value); 201 | return `${enumName}Schema()`; 202 | } 203 | 204 | if (typ && typ.astNode?.kind === 'EnumTypeDefinition') { 205 | const enumName = tsVisitor.convertName(typ.astNode.name.value); 206 | return `${enumName}Schema`; 207 | } 208 | 209 | const primitive = yup4Scalar(config, tsVisitor, node.value); 210 | return primitive; 211 | }; 212 | 213 | const maybeLazy = (type: TypeNode, schema: string): string => { 214 | if (isNamedType(type) && isInput(type.name.value)) { 215 | // https://github.com/jquense/yup/issues/1283#issuecomment-786559444 216 | return `yup.lazy(() => ${schema}) as never`; 217 | } 218 | return schema; 219 | }; 220 | 221 | const maybeNonEmptyString = ( 222 | config: ValidationSchemaPluginConfig, 223 | tsVisitor: TsVisitor, 224 | schema: string, 225 | childType: TypeNode 226 | ): string => { 227 | if (config.notAllowEmptyString === true && isNamedType(childType)) { 228 | const maybeScalarName = childType.name.value; 229 | const tsType = tsVisitor.scalars[maybeScalarName]; 230 | if (tsType === 'string') { 231 | return `${schema}.required()`; 232 | } 233 | } 234 | // fallback 235 | return `${schema}.defined()`; 236 | }; 237 | 238 | const yup4Scalar = ( 239 | config: ValidationSchemaPluginConfig, 240 | tsVisitor: TsVisitor, 241 | scalarName: string 242 | ): string => { 243 | if (config.scalarSchemas?.[scalarName]) { 244 | return config.scalarSchemas[scalarName]; 245 | } 246 | const tsType = tsVisitor.scalars[scalarName]; 247 | switch (tsType) { 248 | case 'string': 249 | return `yup.string()`; 250 | case 'number': 251 | return `yup.number()`; 252 | case 'boolean': 253 | return `yup.boolean()`; 254 | } 255 | console.warn('unhandled name:', scalarName); 256 | return `yup.mixed()`; 257 | }; 258 | -------------------------------------------------------------------------------- /packages/graphql-zod-validation/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ], 13 | "compilerOptions": { 14 | "forceConsistentCasingInFileNames": true, 15 | "strict": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/graphql-zod-validation/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "../../dist/out-tsc", 6 | "declaration": true, 7 | "types": ["node"] 8 | }, 9 | "exclude": ["jest.config.ts", "**/*.spec.ts", "**/*.test.ts"], 10 | "include": ["**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/graphql-zod-validation/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "jest.config.ts", 10 | "**/*.test.ts", 11 | "**/*.spec.ts", 12 | "**/*.test.tsx", 13 | "**/*.spec.tsx", 14 | "**/*.test.js", 15 | "**/*.spec.js", 16 | "**/*.test.jsx", 17 | "**/*.spec.jsx", 18 | "**/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /packages/zod-mock/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@nx/js/babel", 5 | { 6 | "useBuiltIns": "usage" 7 | } 8 | ] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /packages/zod-mock/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/zod-mock/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). 4 | 5 | ## [3.14.0](https://github.com/anatine/zod-plugins/compare/zod-mock-3.13.5...zod-mock-3.14.0) (2025-04-04) 6 | 7 | 8 | ### Features 9 | 10 | * **zod-mock:** add support for readonly fields ([486eb3a](https://github.com/anatine/zod-plugins/commit/486eb3a1f6d3239609388fff5c74c9ed73c5a099)) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * **zod-mock:** update peer dependency range for @faker-js/faker to include ^9.x ([6a89566](https://github.com/anatine/zod-plugins/commit/6a89566d1baae63e20675a703d8318939a5db66e)) 16 | 17 | ### [3.13.5](https://github.com/anatine/zod-plugins/compare/zod-mock-3.13.4...zod-mock-3.13.5) (2025-01-20) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * **zod-mock:** [[#172](https://github.com/anatine/zod-plugins/issues/172)] add support for mockeryMapper to be used for numbers, dates and booleans ([cd50715](https://github.com/anatine/zod-plugins/commit/cd50715c27d9fdb55a32b208357f07568d7ce82e)) 23 | 24 | ### [3.13.4](https://github.com/anatine/zod-plugins/compare/zod-mock-3.13.3...zod-mock-3.13.4) (2024-03-19) 25 | 26 | ### [3.13.3](https://github.com/anatine/zod-plugins/compare/zod-mock-3.13.2...zod-mock-3.13.3) (2023-10-31) 27 | 28 | ### [3.13.2](https://github.com/anatine/zod-plugins/compare/zod-mock-3.13.1...zod-mock-3.13.2) (2023-07-31) 29 | 30 | 31 | ### Bug Fixes 32 | 33 | * fix `nativeEnum` generation ([7e0ed86](https://github.com/anatine/zod-plugins/commit/7e0ed860e2a075c9662537fd05a25a0db6f8bbd2)), closes [#66](https://github.com/anatine/zod-plugins/issues/66) 34 | 35 | ### [3.13.1](https://github.com/anatine/zod-plugins/compare/zod-mock-3.13.0...zod-mock-3.13.1) (2023-06-30) 36 | 37 | ## [3.13.0](https://github.com/anatine/zod-plugins/compare/zod-mock-3.12.1...zod-mock-3.13.0) (2023-06-30) 38 | 39 | 40 | ### Bug Fixes 41 | 42 | * Logic for city key to map to name, not image ([73cac73](https://github.com/anatine/zod-plugins/commit/73cac737da484c3b5bfb360f6000feb11ada3318)) 43 | 44 | ### [3.12.1](https://github.com/anatine/zod-plugins/compare/zod-mock-3.12.0...zod-mock-3.12.1) (2023-06-16) 45 | 46 | ## [3.12.0](https://github.com/anatine/zod-plugins/compare/zod-mock-3.11.0...zod-mock-3.12.0) (2023-05-23) 47 | 48 | 49 | ### Features 50 | 51 | * Dep updates ([156b026](https://github.com/anatine/zod-plugins/commit/156b026391eba70c00df8b0f96ec402db1ceed4c)) 52 | 53 | ## [3.11.0](https://github.com/anatine/zod-plugins/compare/zod-mock-3.10.0...zod-mock-3.11.0) (2023-04-23) 54 | 55 | 56 | ### Features 57 | 58 | * **zod-mock:** pass zodRef and options to generator function which defined in backupMocks ([a7a8acf](https://github.com/anatine/zod-plugins/commit/a7a8acfb31e391106fc314f90c6301733c5c491f)) 59 | 60 | ## [3.10.0](https://github.com/anatine/zod-plugins/compare/zod-mock-3.9.0...zod-mock-3.10.0) (2023-03-22) 61 | 62 | 63 | ### Features 64 | 65 | * extend native enum to support const assertion ([c70336a](https://github.com/anatine/zod-plugins/commit/c70336a16c30492b2e165f438da10528e86a4107)) 66 | 67 | ## [3.9.0](https://github.com/anatine/zod-plugins/compare/zod-mock-3.8.4...zod-mock-3.9.0) (2023-02-27) 68 | 69 | 70 | ### Features 71 | 72 | * **zod-mock:** mock strings with explicit length ([c0f7670](https://github.com/anatine/zod-plugins/commit/c0f767083984d5555dfda6a5c0689eb5b8485e18)) 73 | 74 | ### [3.8.4](https://github.com/anatine/zod-plugins/compare/zod-mock-3.8.3...zod-mock-3.8.4) (2023-02-21) 75 | 76 | 77 | ### Bug Fixes 78 | 79 | * mockData example ([d45a3ec](https://github.com/anatine/zod-plugins/commit/d45a3ec389880b0bb967024efdf33065fc250e43)) 80 | 81 | ### [3.8.3](https://github.com/anatine/zod-plugins/compare/zod-mock-3.8.2...zod-mock-3.8.3) (2023-01-17) 82 | 83 | ### [3.8.2](https://github.com/anatine/zod-plugins/compare/zod-mock-3.8.1...zod-mock-3.8.2) (2023-01-04) 84 | 85 | ### [3.8.1](https://github.com/anatine/zod-plugins/compare/zod-mock-3.8.0...zod-mock-3.8.1) (2023-01-01) 86 | 87 | ## [3.8.0](https://github.com/anatine/zod-plugins/compare/zod-mock-3.7.0...zod-mock-3.8.0) (2022-12-12) 88 | 89 | 90 | ### Features 91 | 92 | * Optional faker class instance as an option ([ec7b505](https://github.com/anatine/zod-plugins/commit/ec7b505f1c010bb173759431c6c5583ff23bc15a)) 93 | 94 | ## [3.7.0](https://github.com/anatine/zod-plugins/compare/zod-mock-3.6.0...zod-mock-3.7.0) (2022-12-12) 95 | 96 | 97 | ### Features 98 | 99 | * Updated Dependencies ([ad8cfc8](https://github.com/anatine/zod-plugins/commit/ad8cfc8fa40ca32736dbfb0d8906569d2a626cbe)) 100 | 101 | 102 | ### Bug Fixes 103 | 104 | * array length in Zod Mock ([2811101](https://github.com/anatine/zod-plugins/commit/2811101302fb70e8e769f5d15345d880495e1485)) 105 | 106 | ## [3.6.0](https://github.com/anatine/zod-plugins/compare/zod-mock-3.5.11...zod-mock-3.6.0) (2022-10-05) 107 | 108 | 109 | ### Features 110 | 111 | * Support for @Param() in @anatine/zod-nestjs ([ba00144](https://github.com/anatine/zod-plugins/commit/ba001444d3554695fe6db6b0d449f03351d65c48)) 112 | 113 | ### [3.5.11](https://github.com/anatine/zod-plugins/compare/zod-mock-3.5.10...zod-mock-3.5.11) (2022-09-12) 114 | 115 | ### [3.5.10](https://github.com/anatine/zod-plugins/compare/zod-mock-3.5.9...zod-mock-3.5.10) (2022-09-12) 116 | 117 | ### [3.5.9](https://github.com/anatine/zod-plugins/compare/zod-mock-3.5.8...zod-mock-3.5.9) (2022-09-05) 118 | 119 | ### [3.5.8](https://github.com/anatine/zod-plugins/compare/zod-mock-3.5.7...zod-mock-3.5.8) (2022-09-05) 120 | 121 | ### [3.5.7](https://github.com/anatine/zod-plugins/compare/zod-mock-3.5.6...zod-mock-3.5.7) (2022-08-17) 122 | 123 | 124 | ### Bug Fixes 125 | 126 | * replace findName to fullName ([8f04cbd](https://github.com/anatine/zod-plugins/commit/8f04cbdf0ffbb4ec98cc930e4ab8b95913736cc1)) 127 | 128 | ### [3.5.6](https://github.com/anatine/zod-plugins/compare/zod-mock-3.5.5...zod-mock-3.5.6) (2022-07-28) 129 | 130 | 131 | ### Bug Fixes 132 | 133 | * nativeEnum random pick function bias ([a082988](https://github.com/anatine/zod-plugins/commit/a0829880212bc43b858ee5ccf06fa7d9986d2479)) 134 | 135 | ### [3.5.6](https://github.com/anatine/zod-plugins/compare/zod-mock-3.5.5...zod-mock-3.5.6) (2022-07-28) 136 | 137 | 138 | ### Bug Fixes 139 | 140 | * nativeEnum random pick function bias ([a082988](https://github.com/anatine/zod-plugins/commit/a0829880212bc43b858ee5ccf06fa7d9986d2479)) 141 | 142 | ### [3.5.5](https://github.com/anatine/zod-plugins/compare/zod-mock-3.5.4...zod-mock-3.5.5) (2022-07-26) 143 | 144 | 145 | ### Bug Fixes 146 | 147 | * nativeEnum parsing ([a07e791](https://github.com/anatine/zod-plugins/commit/a07e79166fac0c53eb9569058f2de4e4b85edfda)) 148 | 149 | ### [3.5.5](https://github.com/anatine/zod-plugins/compare/zod-mock-3.5.4...zod-mock-3.5.5) (2022-07-26) 150 | 151 | 152 | ### Bug Fixes 153 | 154 | * nativeEnum parsing ([a07e791](https://github.com/anatine/zod-plugins/commit/a07e79166fac0c53eb9569058f2de4e4b85edfda)) 155 | 156 | ### [3.5.5](https://github.com/anatine/zod-plugins/compare/zod-mock-3.5.4...zod-mock-3.5.5) (2022-07-26) 157 | 158 | 159 | ### Bug Fixes 160 | 161 | * nativeEnum parsing ([a07e791](https://github.com/anatine/zod-plugins/commit/a07e79166fac0c53eb9569058f2de4e4b85edfda)) 162 | 163 | ### [3.5.5](https://github.com/anatine/zod-plugins/compare/zod-mock-3.5.4...zod-mock-3.5.5) (2022-07-26) 164 | 165 | 166 | ### Bug Fixes 167 | 168 | * nativeEnum parsing ([a07e791](https://github.com/anatine/zod-plugins/commit/a07e79166fac0c53eb9569058f2de4e4b85edfda)) 169 | 170 | ### [3.5.5](https://github.com/anatine/zod-plugins/compare/zod-mock-3.5.4...zod-mock-3.5.5) (2022-07-26) 171 | 172 | 173 | ### Bug Fixes 174 | 175 | * nativeEnum parsing ([a07e791](https://github.com/anatine/zod-plugins/commit/a07e79166fac0c53eb9569058f2de4e4b85edfda)) 176 | 177 | ## [3.5.3](https://github.com/anatine/zod-plugins/compare/zod-mock-3.5.2...zod-mock-3.5.3) (2022-07-26) 178 | 179 | # [3.4.0](https://github.com/anatine/zod-plugins/compare/zod-mock-3.3.0...zod-mock-3.4.0) (2022-07-24) 180 | 181 | ### Bug Fixes 182 | 183 | * native enum error ([1ad2dff](https://github.com/anatine/zod-plugins/commit/1ad2dffbb37e5435581d4d8bdb127b56314700a8)) 184 | * parseNativeEnum arg type ([5700a14](https://github.com/anatine/zod-plugins/commit/5700a142caf0b585bc7f204a4985c7a187d4a316)) 185 | * record type and swap string min when greater than max ([ae51c01](https://github.com/anatine/zod-plugins/commit/ae51c01fb55c3c386c3362680b40df25c3706c14)) 186 | * type issue in zod-mock ([b2cd0bd](https://github.com/anatine/zod-plugins/commit/b2cd0bd2e1192333f928b60fb8bc59a3321522c2)) 187 | 188 | # [3.3.0](https://github.com/anatine/zod-plugins/compare/zod-mock-3.2.2...zod-mock-3.3.0) (2022-07-14) 189 | 190 | ## [0.0.2](https://github.com/anatine/zod-plugins/compare/zod-mock-0.0.1...zod-mock-0.0.2) (2022-07-14) 191 | 192 | ## 0.0.1 (2022-07-14) 193 | 194 | ### Bug Fixes 195 | 196 | * Adding in new release githuib actions ([29a2455](https://github.com/anatine/zod-plugins/commit/29a2455161f7021df9f933d0d8b200a08fe31fde)) 197 | -------------------------------------------------------------------------------- /packages/zod-mock/LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2022 Brian McBride 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 | -------------------------------------------------------------------------------- /packages/zod-mock/README.md: -------------------------------------------------------------------------------- 1 | # @anatine/zod-mock 2 | 3 | Generates a mock data object using [faker.js](https://www.npmjs.com/package/@faker-js/faker) from a [Zod](https://github.com/colinhacks/zod) schema. 4 | 5 | ---- 6 | 7 | ## Installation 8 | 9 | `@faker-js/faker` is a peer dependency of `@anatine/zod-mock` and is used for mock data generation. 10 | 11 | ```shell 12 | npm install zod @faker-js/faker @anatine/zod-mock 13 | ``` 14 | 15 | ---- 16 | 17 | ## Usage 18 | 19 | ### Take any Zod schema and create mock data 20 | 21 | ```typescript 22 | import { generateMock } from '@anatine/zod-mock'; 23 | const schema = z.object({ 24 | uid: z.string().nonempty(), 25 | theme: z.enum([`light`, `dark`]), 26 | email: z.string().email().optional(), 27 | phoneNumber: z.string().min(10).optional(), 28 | avatar: z.string().url().optional(), 29 | jobTitle: z.string().optional(), 30 | otherUserEmails: z.array(z.string().email()), 31 | stringArrays: z.array(z.string()), 32 | stringLength: z.string().transform((val) => val.length), 33 | numberCount: z.number().transform((item) => `total value = ${item}`), 34 | age: z.number().min(18).max(120), 35 | }); 36 | const mockData = generateMock(schema); 37 | // ... 38 | ``` 39 | 40 | This will generate mock data similar to: 41 | 42 | ```json 43 | { 44 | "uid": "3f46b40e-95ed-43d0-9165-0b8730de8d14", 45 | "theme": "light", 46 | "email": "Alexandre99@hotmail.com", 47 | "phoneNumber": "1-665-405-2226", 48 | "avatar": "https://cdn.fakercloud.com/avatars/olaolusoga_128.jpg", 49 | "jobTitle": "Lead Brand Facilitator", 50 | "otherUserEmails": [ 51 | "Wyman58@example.net", 52 | "Ignacio_Nader@example.org", 53 | "Jorge_Bradtke@example.org", 54 | "Elena.Torphy33@example.org", 55 | "Kelli_Bartoletti@example.com" 56 | ], 57 | "stringArrays": [ 58 | "quisquam", 59 | "corrupti", 60 | "atque", 61 | "sunt", 62 | "voluptatem" 63 | ], 64 | "stringLength": 4, 65 | "numberCount": "total value = 25430", 66 | "age": 110 67 | } 68 | ``` 69 | 70 | ---- 71 | 72 | ## Overriding string mocks 73 | 74 | Sometimes there might be a reason to have a more specific mock for a string value. 75 | 76 | You can supply an options field to generate specific mock data that will be triggered by the matching key. 77 | 78 | ```typescript 79 | const schema = z.object({ 80 | locked: z.string(), 81 | email: z.string().email(), 82 | primaryColor: z.string(), 83 | }); 84 | const mockData = generateMock(schema, { 85 | stringMap: { 86 | locked: () => `this return set to the locked value`, 87 | email: () => `not a email anymore`, 88 | primaryColor: () => faker.internet.color(), 89 | }, 90 | }); 91 | ``` 92 | 93 | ---- 94 | 95 | ## Adding a seed generator 96 | 97 | For consistent testing, you can also add in a seed or seed array. 98 | 99 | ```typescript 100 | const schema = z.object({ 101 | name: z.string(), 102 | age: z.number(), 103 | }); 104 | const seed = 123; 105 | const first = generateMock(schema, { seed }); 106 | const second = generateMock(schema, { seed }); 107 | expect(first).toEqual(second); 108 | ``` 109 | 110 | ---- 111 | 112 | ## Adding a custom mock mapper 113 | 114 | Once drilled down to deliver a string, number, boolean, or other primitive value a function with a matching name is searched for in faker. 115 | 116 | You can add your own key/fn mapper in the options. 117 | 118 | ```typescript 119 | 120 | export function mockeryMapper( 121 | keyName: string, 122 | fakerInstance: Faker 123 | ): FakerFunction | undefined { 124 | const keyToFnMap: Record = { 125 | image: fakerInstance.image.url, 126 | imageurl: fakerInstance.image.url, 127 | number: fakerInstance.number.int, 128 | float: fakerInstance.number.float, 129 | hexadecimal: fakerInstance.number.hex, 130 | uuid: fakerInstance.string.uuid, 131 | boolean: fakerInstance.datatype.boolean, 132 | // Email more guaranteed to be random for testing 133 | email: () => fakerInstance.database.mongodbObjectId() + '@example.com' 134 | }; 135 | return keyName && keyName.toLowerCase() in keyToFnMap 136 | ? keyToFnMap[keyName.toLowerCase() as never] 137 | : undefined; 138 | } 139 | 140 | const schema = z.object({ 141 | locked: z.string(), 142 | email: z.string().email(), 143 | primaryColor: z.string(), 144 | }); 145 | 146 | const result = generateMock(schema, { mockeryMapper }); 147 | 148 | ``` 149 | 150 | ---- 151 | 152 | ## Behind the Scenes 153 | 154 | **`zod-mock`** tries to generate mock data from two sources. 155 | 156 | - ### Object key name ie(`{ firstName: z.string() }`) 157 | 158 | This will check the string name of the key against all the available `faker` function names. 159 | Upon a match, it uses that function to generate a mock value. 160 | 161 | - ### Zodtype ie(`const something = z.string()`) 162 | 163 | In the case there is no key name (the schema doesn't contain an object) or there is no key name match, 164 | `zod-mock` will use the primitive type provided by `zod`. 165 | 166 | Some zod filter types (email, uuid, url, min, max, length) will also modify the results. 167 | 168 | If **`zod-mock`** does not yet support a Zod type used in your schema, you may provide a backup mock function to use for that particular type. 169 | 170 | ``` typescript 171 | const schema = z.object({ 172 | anyVal: z.any() 173 | }); 174 | const mockData = generateMock(schema, { 175 | backupMocks: { 176 | ZodAny: () => 'a value' 177 | } 178 | }); 179 | ``` 180 | 181 | ---- 182 | 183 | ## Missing Features 184 | 185 | - No pattern for passing options into `faker`, such as setting phone number formatting 186 | - Does not handle the following Zod types: 187 | - ZodAny 188 | - ZodDefault 189 | - ZodFunction 190 | - ZodIntersection 191 | - ZodMap 192 | - ZodPromise 193 | - ZodSet 194 | - ZodTuple 195 | - ZodUnion 196 | - ZodUnknown 197 | 198 | ---- 199 | 200 | ## Credits 201 | 202 | - ### [express-zod-api](https://github.com/RobinTail/express-zod-api) 203 | 204 | A great lib that provided some insights on dealing with various zod types. 205 | 206 | ---- 207 | 208 | This library is part of a nx monorepo [@anatine/zod-plugins](https://github.com/anatine/zod-plugins). 209 | -------------------------------------------------------------------------------- /packages/zod-mock/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'zod-mock', 4 | preset: '../../jest.preset.js', 5 | globals: {}, 6 | testEnvironment: 'node', 7 | transform: { 8 | '^.+\\.[tj]sx?$': [ 9 | 'ts-jest', 10 | { 11 | tsconfig: '/tsconfig.spec.json', 12 | }, 13 | ], 14 | }, 15 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 16 | coverageDirectory: '../../coverage/packages/zod-mock', 17 | }; 18 | -------------------------------------------------------------------------------- /packages/zod-mock/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@anatine/zod-mock", 3 | "version": "3.14.0", 4 | "description": "Zod auto-mock object generator using Faker at @faker-js/faker", 5 | "main": "src/index.js", 6 | "types": "src/index.d.ts", 7 | "license": "MIT", 8 | "public": true, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/anatine/zod-plugins" 12 | }, 13 | "homepage": "https://github.com/anatine/zod-plugins/tree/main/packages/zod-mock", 14 | "author": { 15 | "name": "Brian McBride", 16 | "url": "https://www.linkedin.com/in/brianmcbride" 17 | }, 18 | "keywords": [ 19 | "zod", 20 | "mock", 21 | "faker-js" 22 | ], 23 | "peerDependencies": { 24 | "@faker-js/faker": "^7.0.0 || ^8.0.0 || ^9.0.0", 25 | "zod": "^3.21.4" 26 | }, 27 | "dependencies": { 28 | "randexp": "^0.5.3" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/zod-mock/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zod-mock", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "packages/zod-mock/src", 5 | "projectType": "library", 6 | "targets": { 7 | "build": { 8 | "executor": "@nx/js:tsc", 9 | "outputs": ["{options.outputPath}"], 10 | "options": { 11 | "outputPath": "dist/packages/zod-mock", 12 | "tsConfig": "packages/zod-mock/tsconfig.lib.json", 13 | "packageJson": "packages/zod-mock/package.json", 14 | "main": "packages/zod-mock/src/index.ts", 15 | "assets": ["packages/zod-mock/*.md"] 16 | } 17 | }, 18 | "lint": { 19 | "executor": "@nx/linter:eslint", 20 | "outputs": ["{options.outputFile}"], 21 | "options": { 22 | "lintFilePatterns": ["packages/zod-mock/**/*.ts"] 23 | } 24 | }, 25 | "test": { 26 | "executor": "@nx/jest:jest", 27 | "outputs": ["{workspaceRoot}/coverage/packages/zod-mock"], 28 | "options": { 29 | "jestConfig": "packages/zod-mock/jest.config.ts", 30 | "passWithNoTests": true 31 | } 32 | }, 33 | "version": { 34 | "executor": "@jscutlery/semver:version", 35 | "options": { 36 | "push": true, 37 | "preset": "conventional", 38 | "skipCommitTypes": ["ci"], 39 | "postTargets": ["zod-mock:build", "zod-mock:publish", "zod-mock:github"] 40 | } 41 | }, 42 | "github": { 43 | "executor": "@jscutlery/semver:github", 44 | "options": { 45 | "tag": "${tag}", 46 | "notes": "${notes}" 47 | } 48 | }, 49 | "publish": { 50 | "executor": "ngx-deploy-npm:deploy", 51 | "options": { 52 | "access": "public", 53 | "distFolderPath": "dist/packages/zod-mock" 54 | }, 55 | "dependsOn": ["build"] 56 | } 57 | }, 58 | "tags": [] 59 | } 60 | -------------------------------------------------------------------------------- /packages/zod-mock/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/zod-mock'; 2 | export * from './lib/zod-mockery-map'; 3 | -------------------------------------------------------------------------------- /packages/zod-mock/src/lib/zod-mockery-map.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-types */ 2 | /* eslint-disable @typescript-eslint/no-unused-vars */ 3 | /** 4 | * This serves as a config file for mapping keynames to mock functions. 5 | */ 6 | import { type Faker } from '@faker-js/faker'; 7 | 8 | export type FakerFunction = () => string | number | boolean | Date; 9 | 10 | export type MockeryMapper = ( 11 | keyName: string, 12 | fakerInstance: Faker 13 | ) => FakerFunction | undefined; 14 | 15 | export function mockeryMapper( 16 | keyName: string, 17 | fakerInstance: Faker 18 | ): FakerFunction | undefined { 19 | const keyToFnMap: Record = { 20 | image: fakerInstance.image.url, 21 | imageurl: fakerInstance.image.url, 22 | number: fakerInstance.number.int, 23 | float: fakerInstance.number.float, 24 | hexadecimal: fakerInstance.number.hex, 25 | uuid: fakerInstance.string.uuid, 26 | boolean: fakerInstance.datatype.boolean, 27 | city: fakerInstance.location.city, 28 | }; 29 | 30 | return keyName && keyName.toLowerCase() in keyToFnMap 31 | ? keyToFnMap[keyName.toLowerCase() as never] 32 | : undefined; 33 | } 34 | -------------------------------------------------------------------------------- /packages/zod-mock/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ], 13 | "compilerOptions": { 14 | "forceConsistentCasingInFileNames": true, 15 | "strict": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/zod-mock/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "../../dist/out-tsc", 6 | "declaration": true, 7 | "types": ["node"] 8 | }, 9 | "exclude": ["jest.config.ts", "**/*.spec.ts", "**/*.test.ts"], 10 | "include": ["**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/zod-mock/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "jest.config.ts", 10 | "**/*.test.ts", 11 | "**/*.spec.ts", 12 | "**/*.test.tsx", 13 | "**/*.spec.tsx", 14 | "**/*.test.js", 15 | "**/*.spec.js", 16 | "**/*.test.jsx", 17 | "**/*.spec.jsx", 18 | "**/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /packages/zod-nestjs/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@nx/js/babel", 5 | { 6 | "useBuiltIns": "usage" 7 | } 8 | ] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /packages/zod-nestjs/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/zod-nestjs/LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2022 Brian McBride 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 | -------------------------------------------------------------------------------- /packages/zod-nestjs/README.md: -------------------------------------------------------------------------------- 1 | # @anatine/zod-nestjs 2 | 3 | Helper methods for using [Zod](https://github.com/colinhacks/zod) in a NestJS project. 4 | 5 | - Validation pipe on data 6 | - Patch to Swagger module 7 | 8 | ---- 9 | 10 | ## Installation 11 | 12 | @anatine/zod-openapi, openapi3-ts, and zod are peer dependencies instead of dependant packages. 13 | While `zod` is necessary for operation, `openapi3-ts` is for type-casting. `@anatine/zod-openapi` does the actual conversion 14 | 15 | ```shell 16 | npm install openapi3-ts zod @anatine/zod-openapi @anatine/zod-nestjs 17 | ``` 18 | 19 | ---- 20 | 21 | ## Usage 22 | 23 | ### Generate a schema 24 | 25 | Use [Zod](https://github.com/colinhacks/zod) to generate a schema. 26 | Additionally, use [@anatidae/zod-openapi](https://github.com/anatine/zod-plugins/tree/main/libs/zod-openapi) to extend a schema for OpenAPI and Swagger UI. 27 | 28 | Example schema: 29 | 30 | ```typescript 31 | import { createZodDto } from '@anatine/zod-nestjs'; 32 | import { extendApi } from '@anatine/zod-openapi'; 33 | import { z } from 'zod'; 34 | export const CatZ = extendApi( 35 | z.object({ 36 | name: z.string(), 37 | age: z.number(), 38 | breed: z.string(), 39 | }), 40 | { 41 | title: 'Cat', 42 | description: 'A cat', 43 | } 44 | ); 45 | export class CatDto extends createZodDto(CatZ) {} 46 | export class UpdateCatDto extends createZodDto(CatZ.omit({ name: true })) {} 47 | export const GetCatsZ = extendApi( 48 | z.object({ 49 | cats: extendApi(z.array(z.string()), { description: 'List of cats' }), 50 | }), 51 | { title: 'Get Cat Response' } 52 | ); 53 | export class GetCatsDto extends createZodDto(GetCatsZ) {} 54 | export const CreateCatResponseZ = z.object({ 55 | success: z.boolean(), 56 | message: z.string(), 57 | name: z.string(), 58 | }); 59 | export class CreateCatResponseDto extends createZodDto(CreateCatResponseZ) {} 60 | export class UpdateCatResponseDto extends createZodDto( 61 | CreateCatResponseZ.omit({ name: true }) 62 | ) {} 63 | ``` 64 | 65 | ### Use the schema in your controller 66 | 67 | This follows the standard NestJS method of creating controllers. 68 | 69 | `@nestjs/swagger` decorators should work normally. 70 | 71 | Example Controller 72 | 73 | ```typescript 74 | import { ZodValidationPipe } from '@anatine/zod-nestjs'; 75 | import { 76 | Body, 77 | Controller, 78 | Get, 79 | Param, 80 | Patch, 81 | Post, 82 | UsePipes, 83 | } from '@nestjs/common'; 84 | import { ApiCreatedResponse } from '@nestjs/swagger'; 85 | import { 86 | CatDto, 87 | CreateCatResponseDto, 88 | GetCatsDto, 89 | UpdateCatDto, 90 | UpdateCatResponseDto, 91 | } from './cats.dto'; 92 | @Controller('cats') 93 | @UsePipes(ZodValidationPipe) 94 | export class CatsController { 95 | @Get() 96 | @ApiCreatedResponse({ 97 | type: GetCatsDto, 98 | }) 99 | async findAll(): Promise { 100 | return { cats: ['Lizzie', 'Spike'] }; 101 | } 102 | @Get(':id') 103 | @ApiCreatedResponse({ 104 | type: CatDto, 105 | }) 106 | async findOne(@Param() { id }: { id: string }): Promise { 107 | return { 108 | name: `Cat-${id}`, 109 | age: 8, 110 | breed: 'Unknown', 111 | }; 112 | } 113 | @Post() 114 | @ApiCreatedResponse({ 115 | description: 'The record has been successfully created.', 116 | type: CreateCatResponseDto, 117 | }) 118 | async create(@Body() createCatDto: CatDto): Promise { 119 | return { 120 | success: true, 121 | message: 'Cat created', 122 | name: createCatDto.name, 123 | }; 124 | } 125 | @Patch() 126 | async update( 127 | @Body() updateCatDto: UpdateCatDto 128 | ): Promise { 129 | return { 130 | success: true, 131 | message: `Cat's age of ${updateCatDto.age} updated`, 132 | }; 133 | } 134 | } 135 | ``` 136 | 137 | NOTE: Responses have to use the `ApiCreatedResponse` decorator when using the `@nestjs/swagger` module. 138 | 139 | ### Set up your app 140 | 141 | Patch the swagger so that it can use Zod types before you create the document. 142 | 143 | Example Main App 144 | 145 | ```typescript 146 | import { Logger } from '@nestjs/common'; 147 | import { NestFactory } from '@nestjs/core'; 148 | import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; 149 | import { CatsModule } from './app/cats.module'; 150 | import { patchNestjsSwagger } from '@anatine/zod-nestjs'; 151 | async function bootstrap() { 152 | const app = await NestFactory.create(CatsModule); 153 | const globalPrefix = 'api'; 154 | app.setGlobalPrefix(globalPrefix); 155 | const config = new DocumentBuilder() 156 | .setTitle('Cats example') 157 | .setDescription('The cats API description') 158 | .setVersion('1.0') 159 | .addTag('cats') 160 | .build(); 161 | patchNestjsSwagger(); // <--- This is the hacky patch using prototypes (for now) 162 | const document = SwaggerModule.createDocument(app, config); 163 | SwaggerModule.setup('api', app, document); 164 | const port = process.env.PORT || 3333; 165 | await app.listen(port, () => { 166 | Logger.log('Listening at http://localhost:' + port + '/' + globalPrefix); 167 | }); 168 | } 169 | bootstrap(); 170 | ``` 171 | 172 | ## Future goals 173 | 174 | - Remove dependency on `@nestjs/swagger` by providing a Swagger UI. 175 | - Expand to create an express-only wrapper (without NestJS) 176 | - Auto generate client side libs with Zod validation. 177 | 178 | ## Credits 179 | 180 | - ### [zod-dto](https://github.com/kbkk/abitia/tree/master/packages/zod-dto) 181 | 182 | Extensive use and inspiration from zod-dto. 183 | -------------------------------------------------------------------------------- /packages/zod-nestjs/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'zod-nestjs', 4 | preset: '../../jest.preset.js', 5 | globals: {}, 6 | testEnvironment: 'node', 7 | transform: { 8 | '^.+\\.[tj]sx?$': [ 9 | 'ts-jest', 10 | { 11 | tsconfig: '/tsconfig.spec.json', 12 | }, 13 | ], 14 | }, 15 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 16 | coverageDirectory: '../../coverage/packages/zod-nestjs', 17 | }; 18 | -------------------------------------------------------------------------------- /packages/zod-nestjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@anatine/zod-nestjs", 3 | "version": "2.0.12", 4 | "description": "Zod helper methods for NestJS", 5 | "main": "src/index.js", 6 | "types": "src/index.d.ts", 7 | "license": "MIT", 8 | "public": true, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/anatine/zod-plugins" 12 | }, 13 | "homepage": "https://github.com/anatine/zod-plugins/tree/main/packages/zod-nestjs", 14 | "author": { 15 | "name": "Brian McBride", 16 | "url": "https://www.linkedin.com/in/brianmcbride" 17 | }, 18 | "keywords": [ 19 | "zod", 20 | "openapi", 21 | "swagger", 22 | "nestjs" 23 | ], 24 | "dependencies": {}, 25 | "peerDependencies": { 26 | "zod": "^3.20.0", 27 | "openapi3-ts": "^4.1.2", 28 | "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", 29 | "@nestjs/swagger": "^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", 30 | "@anatine/zod-openapi": "^2.0.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/zod-nestjs/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zod-nestjs", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "packages/zod-nestjs/src", 5 | "projectType": "library", 6 | "targets": { 7 | "build": { 8 | "executor": "@nx/js:tsc", 9 | "outputs": ["{options.outputPath}"], 10 | "options": { 11 | "outputPath": "dist/packages/zod-nestjs", 12 | "tsConfig": "packages/zod-nestjs/tsconfig.lib.json", 13 | "packageJson": "packages/zod-nestjs/package.json", 14 | "main": "packages/zod-nestjs/src/index.ts", 15 | "assets": ["packages/zod-nestjs/*.md"] 16 | } 17 | }, 18 | "lint": { 19 | "executor": "@nx/linter:eslint", 20 | "outputs": ["{options.outputFile}"], 21 | "options": { 22 | "lintFilePatterns": ["packages/zod-nestjs/**/*.ts"] 23 | } 24 | }, 25 | "test": { 26 | "executor": "@nx/jest:jest", 27 | "outputs": ["{workspaceRoot}/coverage/packages/zod-nestjs"], 28 | "options": { 29 | "jestConfig": "packages/zod-nestjs/jest.config.ts", 30 | "passWithNoTests": true 31 | } 32 | }, 33 | "version": { 34 | "executor": "@jscutlery/semver:version", 35 | "options": { 36 | "push": true, 37 | "preset": "conventional", 38 | "skipCommitTypes": ["ci"], 39 | "postTargets": [ 40 | "zod-nestjs:build", 41 | "zod-nestjs:publish", 42 | "zod-nestjs:github" 43 | ] 44 | } 45 | }, 46 | "github": { 47 | "executor": "@jscutlery/semver:github", 48 | "options": { 49 | "tag": "${tag}", 50 | "notes": "${notes}" 51 | } 52 | }, 53 | "publish": { 54 | "executor": "ngx-deploy-npm:deploy", 55 | "options": { 56 | "access": "public", 57 | "distFolderPath": "dist/packages/zod-nestjs" 58 | }, 59 | "dependsOn": ["build"] 60 | } 61 | }, 62 | "tags": [] 63 | } 64 | -------------------------------------------------------------------------------- /packages/zod-nestjs/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/create-zod-dto'; 2 | export * from './lib/zod-validation-pipe'; 3 | export * from './lib/patch-nest-swagger'; 4 | -------------------------------------------------------------------------------- /packages/zod-nestjs/src/lib/create-zod-dto.spec.ts: -------------------------------------------------------------------------------- 1 | import * as z from 'zod'; 2 | import { ZodError } from 'zod'; 3 | 4 | import { createZodDto } from './create-zod-dto'; 5 | import { SchemaObject as SchemaObject30 } from 'openapi3-ts/oas30'; 6 | import { OpenApiZodAny } from '@anatine/zod-openapi'; 7 | 8 | describe('zod-nesjs create-zod-dto', () => { 9 | const testDtoSchema = z.object({ 10 | val: z.string(), 11 | }); 12 | 13 | it('should create a DTO', () => { 14 | class TestDto extends createZodDto(testDtoSchema) {} 15 | const result = TestDto.create({ val: 'test' }); 16 | expect(result).toEqual({ val: 'test' }); 17 | }); 18 | 19 | it('should throw if input does not match schema', () => { 20 | class TestDto extends createZodDto(testDtoSchema) {} 21 | expect(() => TestDto.create({ val: 123 })).toThrow(ZodError); 22 | }); 23 | 24 | it('should allow using the zodSchema field to construct other types', () => { 25 | class TestDto extends createZodDto(testDtoSchema) {} 26 | expect( 27 | TestDto.zodSchema.extend({ extraField: z.string() }).parse({ 28 | val: 'test', 29 | extraField: 'extra', 30 | unrecognizedField: 'unrecognized', 31 | }) 32 | ).toEqual({ val: 'test', extraField: 'extra' }); 33 | }); 34 | 35 | it('should merge a discriminated union types for class', () => { 36 | enum Kind { 37 | A, 38 | B, 39 | } 40 | const discriminatedSchema = z.discriminatedUnion('kind', [ 41 | z.object({ 42 | kind: z.literal(Kind.A), 43 | value: z.number(), 44 | }), 45 | z.object({ 46 | kind: z.literal(Kind.B), 47 | value: z.string(), 48 | }), 49 | ]); 50 | 51 | class TestDto extends createZodDto(discriminatedSchema) {} 52 | 53 | const result = TestDto.create({ kind: Kind.A, value: 1 }); 54 | expect(result).toEqual({ kind: Kind.A, value: 1 }); 55 | }); 56 | 57 | it('should merge the union types for class', () => { 58 | enum Kind { 59 | A, 60 | B, 61 | } 62 | const unionSchema = z.union([ 63 | z.object({ 64 | kind: z.literal(Kind.A), 65 | value: z.number(), 66 | }), 67 | z.object({ 68 | kind: z.literal(Kind.B), 69 | value: z.string(), 70 | }), 71 | ]); 72 | 73 | class TestDto extends createZodDto(unionSchema) {} 74 | 75 | const result = TestDto.create({ kind: Kind.B, value: 'val' }); 76 | expect(result).toEqual({ kind: Kind.B, value: 'val' }); 77 | }); 78 | 79 | it('should output OpenAPI 3.0-style nullable types', () => { 80 | const schema = z.object({ 81 | name: z.string().nullable(), 82 | }); 83 | const metadataFactory = getMetadataFactory(schema); 84 | 85 | const generatedSchema = metadataFactory(); 86 | 87 | expect(generatedSchema).toBeDefined(); 88 | expect(generatedSchema?.name.type).toEqual('string'); 89 | expect(generatedSchema?.name.nullable).toBe(true); 90 | }); 91 | 92 | it('should output OpenAPI 3.0-style exclusive minimum and maximum types', () => { 93 | const schema = z.object({ 94 | inclusive: z.number().min(1).max(10), 95 | exclusive: z.number().gt(1).lt(10), 96 | unlimited: z.number(), 97 | }); 98 | const metadataFactory = getMetadataFactory(schema); 99 | 100 | const generatedSchema = metadataFactory(); 101 | 102 | expect(generatedSchema).toBeDefined(); 103 | expect(generatedSchema?.inclusive.minimum).toBe(1); 104 | expect(generatedSchema?.inclusive.exclusiveMinimum).toBeUndefined(); 105 | expect(generatedSchema?.inclusive.maximum).toBe(10); 106 | expect(generatedSchema?.inclusive.exclusiveMaximum).toBeUndefined(); 107 | expect(generatedSchema?.exclusive.minimum).toBe(1); 108 | expect(generatedSchema?.exclusive.exclusiveMinimum).toBe(true); 109 | expect(generatedSchema?.exclusive.maximum).toBe(10); 110 | expect(generatedSchema?.exclusive.exclusiveMaximum).toBe(true); 111 | expect(generatedSchema?.unlimited.minimum).toBeUndefined(); 112 | expect(generatedSchema?.unlimited.exclusiveMinimum).toBeUndefined(); 113 | expect(generatedSchema?.unlimited.maximum).toBeUndefined(); 114 | expect(generatedSchema?.unlimited.exclusiveMaximum).toBeUndefined(); 115 | }); 116 | 117 | it('should convert to OpenAPI 3.0 in deep objects and arrays', () => { 118 | const schema = z.object({ 119 | person: z.object({ 120 | name: z.string().nullable(), 121 | tags: z.array( 122 | z.object({ id: z.string(), name: z.string().nullable() }) 123 | ), 124 | }), 125 | }); 126 | const metadataFactory = getMetadataFactory(schema); 127 | 128 | const generatedSchema = metadataFactory(); 129 | const personName = generatedSchema?.person.properties 130 | ?.name as SchemaObject30; 131 | const tags = generatedSchema?.person.properties?.tags as SchemaObject30; 132 | const tagsItems = tags.items as SchemaObject30; 133 | const tagName = tagsItems.properties?.name as SchemaObject30; 134 | 135 | expect(generatedSchema).toBeDefined(); 136 | expect(personName.type).toEqual('string'); 137 | expect(personName.nullable).toBe(true); 138 | expect(tagName.type).toBe('string'); 139 | expect(tagName.nullable).toBe(true); 140 | }); 141 | 142 | it('should convert literal null value to OpenAPI 3.0', () => { 143 | const schema = z.object({ 144 | name: z.null(), 145 | }); 146 | const metadataFactory = getMetadataFactory(schema); 147 | 148 | const generatedSchema = metadataFactory(); 149 | 150 | expect(generatedSchema).toBeDefined(); 151 | expect(generatedSchema?.name.type).toEqual('string'); 152 | expect(generatedSchema?.name.nullable).toBe(true); 153 | }); 154 | 155 | it('should correct work with optional fields and make required fields false', () => { 156 | const schema = z.object({ 157 | pagination: z 158 | .object({ 159 | limit: z.number(), 160 | offset: z.number(), 161 | }) 162 | .optional(), 163 | filter: z 164 | .object({ 165 | category: z.string().uuid(), 166 | userId: z.string().uuid(), 167 | }) 168 | .optional(), 169 | sort: z 170 | .object({ 171 | field: z.string(), 172 | order: z.string(), 173 | }) 174 | .optional(), 175 | }); 176 | const metadataFactory = getMetadataFactory(schema); 177 | 178 | const generatedSchema = metadataFactory(); 179 | expect(generatedSchema?.pagination.required).toEqual(false); 180 | expect(generatedSchema?.sort.required).toEqual(false); 181 | expect(generatedSchema?.filter.required).toEqual(false); 182 | }); 183 | 184 | it('should correct work with optional fields and make required field true and optional field false', () => { 185 | const schema = z.object({ 186 | pagination: z 187 | .object({ 188 | limit: z.number(), 189 | offset: z.number(), 190 | }) 191 | .optional(), 192 | filter: z 193 | .object({ 194 | category: z.string().uuid(), 195 | userId: z.string().uuid(), 196 | }) 197 | .optional(), 198 | sort: z.object({ 199 | field: z.string(), 200 | order: z.string(), 201 | }), 202 | }); 203 | const metadataFactory = getMetadataFactory(schema); 204 | 205 | const generatedSchema = metadataFactory(); 206 | expect(generatedSchema?.pagination.required).toEqual(false); 207 | expect(generatedSchema?.sort.required).toEqual(true); 208 | expect(generatedSchema?.filter.required).toEqual(false); 209 | }); 210 | }); 211 | 212 | function getMetadataFactory(zodRef: OpenApiZodAny) { 213 | const schemaHolderClass = createZodDto(zodRef) as unknown as { 214 | _OPENAPI_METADATA_FACTORY: () => Record | undefined; 215 | }; 216 | return schemaHolderClass._OPENAPI_METADATA_FACTORY; 217 | } 218 | -------------------------------------------------------------------------------- /packages/zod-nestjs/src/lib/create-zod-dto.ts: -------------------------------------------------------------------------------- 1 | import type { SchemaObject as SchemaObject30 } from 'openapi3-ts/oas30'; 2 | import type { 3 | ReferenceObject, 4 | SchemaObject as SchemaObject31, 5 | } from 'openapi3-ts/oas31'; 6 | import { generateSchema, OpenApiZodAny } from '@anatine/zod-openapi'; 7 | import * as z from 'zod'; 8 | 9 | import type { TupleToUnion, Merge } from './types'; 10 | 11 | /** 12 | * This file was originally taken from: 13 | * https://github.com/kbkk/abitia/blob/master/packages/zod-dto/src/createZodDto.ts 14 | * 15 | * It is used to create a DTO from a Zod object. 16 | * I assume that the create method is called within NestJS. 17 | */ 18 | 19 | /** 20 | * ZodType is a very complex interface describing not just public properties but private ones as well 21 | * causing the interface to change fairly often among versions 22 | * 23 | * Since we're interested in the main subset of Zod functionality (type inferring + parsing) this type is introduced 24 | * to achieve the most compatibility. 25 | */ 26 | export type CompatibleZodType = Pick< 27 | z.ZodType, 28 | '_input' | '_output' | 'parse' | 'safeParse' 29 | >; 30 | export type CompatibleZodInfer = T['_output']; 31 | 32 | export type MergeZodSchemaOutput = 33 | T extends z.ZodDiscriminatedUnion 34 | ? Merge< 35 | object, 36 | TupleToUnion<{ 37 | [X in keyof Options]: Options[X] extends z.ZodType 38 | ? Options[X]['_output'] 39 | : Options[X]; 40 | }> 41 | > 42 | : T extends z.ZodUnion 43 | ? UnionTypes extends z.ZodType[] 44 | ? Merge< 45 | object, 46 | TupleToUnion<{ 47 | [X in keyof UnionTypes]: UnionTypes[X] extends z.ZodType 48 | ? UnionTypes[X]['_output'] 49 | : UnionTypes[X]; 50 | }> 51 | > 52 | : T['_output'] 53 | : T['_output']; 54 | 55 | export type ZodDtoStatic = { 56 | new (): MergeZodSchemaOutput; 57 | zodSchema: T; 58 | create(input: unknown): CompatibleZodInfer; 59 | }; 60 | 61 | // Used for transforming the SchemaObject in _OPENAPI_METADATA_FACTORY 62 | type SchemaObjectForMetadataFactory = Omit & { 63 | required: boolean | string[]; 64 | isArray?: boolean; 65 | }; 66 | 67 | export const createZodDto = ( 68 | zodSchema: T 69 | ): ZodDtoStatic => { 70 | class SchemaHolderClass { 71 | public static zodSchema = zodSchema; 72 | schema: SchemaObject31 | undefined; 73 | 74 | constructor() { 75 | this.schema = generateSchema(zodSchema); 76 | } 77 | 78 | /** Found from METADATA_FACTORY_NAME 79 | * in Nestjs swagger module. 80 | * https://github.com/nestjs/swagger/blob/491b168cbff3003191e55ee96e77e69d8c1deb66/lib/type-helpers/mapped-types.utils.ts 81 | * METADATA_FACTORY_NAME is defined here as '_OPENAPI_METADATA_FACTORY' here: 82 | * https://github.com/nestjs/swagger/blob/491b168cbff3003191e55ee96e77e69d8c1deb66/lib/plugin/plugin-constants.ts 83 | */ 84 | public static _OPENAPI_METADATA_FACTORY(): 85 | | Record 86 | | undefined { 87 | const generatedSchema = generateSchema(zodSchema); 88 | SchemaHolderClass.convertSchemaObject(generatedSchema); 89 | return generatedSchema.properties as Record; 90 | } 91 | 92 | private static convertSchemaObject( 93 | schemaObject: SchemaObject31 | ReferenceObject, 94 | required?: boolean 95 | ): void { 96 | if ('$ref' in schemaObject) { 97 | return; 98 | } 99 | 100 | // Recursively convert all sub-schemas 101 | const subSchemaObjects = [ 102 | ...(schemaObject.allOf ?? []), 103 | ...(schemaObject.oneOf ?? []), 104 | ...(schemaObject.anyOf ?? []), 105 | ...(schemaObject.not ? [schemaObject.not] : []), 106 | ...(schemaObject.items ? [schemaObject.items] : []), 107 | ...(typeof schemaObject.additionalProperties === 'object' 108 | ? [schemaObject.additionalProperties] 109 | : []), 110 | ...(schemaObject.prefixItems ?? []), 111 | ]; 112 | for (const subSchemaObject of subSchemaObjects) { 113 | SchemaHolderClass.convertSchemaObject(subSchemaObject); 114 | } 115 | 116 | for (const [key, subSchemaObject] of Object.entries( 117 | schemaObject.properties ?? {} 118 | )) { 119 | SchemaHolderClass.convertSchemaObject( 120 | subSchemaObject, 121 | schemaObject.required?.includes(key) ?? false 122 | ); 123 | } 124 | 125 | /** For some reason the SchemaObject model has everything except for the 126 | * required field, which is an array. 127 | * The NestJS swagger module requires this to be a boolean representative 128 | * of each property. 129 | * This logic takes the SchemaObject, and turns the required field from an 130 | * array to a boolean. 131 | */ 132 | 133 | const convertedSchemaObject = 134 | schemaObject as SchemaObjectForMetadataFactory; 135 | 136 | if (required !== undefined) { 137 | convertedSchemaObject.required = required; 138 | } 139 | 140 | // @nestjs/swagger expects OpenAPI 3.0-style schema objects 141 | // Nullable 142 | if (Array.isArray(convertedSchemaObject.type)) { 143 | convertedSchemaObject.nullable = 144 | convertedSchemaObject.type.includes('null') || undefined; 145 | convertedSchemaObject.type = 146 | convertedSchemaObject.type.find((item) => item !== 'null') || 147 | 'string'; 148 | } else if (convertedSchemaObject.type === 'null') { 149 | convertedSchemaObject.type = 'string'; // There ist no explicit null value in OpenAPI 3.0 150 | convertedSchemaObject.nullable = true; 151 | } 152 | // Array handling (NestJS references 'isArray' boolean) 153 | if (convertedSchemaObject.type === 'array') { 154 | convertedSchemaObject.isArray = true; 155 | } 156 | // Exclusive minimum and maximum 157 | const { exclusiveMinimum, exclusiveMaximum } = schemaObject; 158 | if (exclusiveMinimum !== undefined) { 159 | convertedSchemaObject.minimum = exclusiveMinimum; 160 | convertedSchemaObject.exclusiveMinimum = true; 161 | } 162 | if (exclusiveMaximum !== undefined) { 163 | convertedSchemaObject.maximum = exclusiveMaximum; 164 | convertedSchemaObject.exclusiveMaximum = true; 165 | } 166 | } 167 | 168 | public static create(input: unknown): CompatibleZodInfer { 169 | return this.zodSchema.parse(input); 170 | } 171 | } 172 | 173 | return >SchemaHolderClass; 174 | }; 175 | -------------------------------------------------------------------------------- /packages/zod-nestjs/src/lib/http-errors.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadGatewayException, 3 | BadRequestException, 4 | ConflictException, 5 | ForbiddenException, 6 | GatewayTimeoutException, 7 | GoneException, 8 | HttpStatus, 9 | ImATeapotException, 10 | InternalServerErrorException, 11 | MethodNotAllowedException, 12 | NotAcceptableException, 13 | NotFoundException, 14 | NotImplementedException, 15 | PayloadTooLargeException, 16 | PreconditionFailedException, 17 | RequestTimeoutException, 18 | ServiceUnavailableException, 19 | UnauthorizedException, 20 | UnprocessableEntityException, 21 | UnsupportedMediaTypeException, 22 | } from '@nestjs/common'; 23 | 24 | // This is the same list of HTTP errors as used by the NestJS ValidationPipe. 25 | // https://github.com/nestjs/nest/blob/85b0dc84953802d33ae8753df02adb84d6d0f0e8/packages/common/utils/http-error-by-code.util.ts#L46 26 | 27 | export const HTTP_ERRORS_BY_CODE = { 28 | [HttpStatus.BAD_GATEWAY]: BadGatewayException, 29 | [HttpStatus.BAD_REQUEST]: BadRequestException, 30 | [HttpStatus.CONFLICT]: ConflictException, 31 | [HttpStatus.FORBIDDEN]: ForbiddenException, 32 | [HttpStatus.GATEWAY_TIMEOUT]: GatewayTimeoutException, 33 | [HttpStatus.GONE]: GoneException, 34 | [HttpStatus.I_AM_A_TEAPOT]: ImATeapotException, 35 | [HttpStatus.INTERNAL_SERVER_ERROR]: InternalServerErrorException, 36 | [HttpStatus.METHOD_NOT_ALLOWED]: MethodNotAllowedException, 37 | [HttpStatus.NOT_ACCEPTABLE]: NotAcceptableException, 38 | [HttpStatus.NOT_FOUND]: NotFoundException, 39 | [HttpStatus.NOT_IMPLEMENTED]: NotImplementedException, 40 | [HttpStatus.PAYLOAD_TOO_LARGE]: PayloadTooLargeException, 41 | [HttpStatus.PRECONDITION_FAILED]: PreconditionFailedException, 42 | [HttpStatus.REQUEST_TIMEOUT]: RequestTimeoutException, 43 | [HttpStatus.SERVICE_UNAVAILABLE]: ServiceUnavailableException, 44 | [HttpStatus.UNAUTHORIZED]: UnauthorizedException, 45 | [HttpStatus.UNPROCESSABLE_ENTITY]: UnprocessableEntityException, 46 | [HttpStatus.UNSUPPORTED_MEDIA_TYPE]: UnsupportedMediaTypeException, 47 | } as const; 48 | -------------------------------------------------------------------------------- /packages/zod-nestjs/src/lib/patch-nest-swagger.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | /* eslint-disable @typescript-eslint/ban-types */ 3 | 4 | /** 5 | * This file was copied from: 6 | * https://github.com/kbkk/abitia/blob/master/packages/zod-dto/src/OpenApi/patchNestjsSwagger.ts 7 | */ 8 | import {generateSchema} from '@anatine/zod-openapi'; 9 | import type {SchemaObject} from 'openapi3-ts/oas31'; 10 | 11 | interface Type extends Function { 12 | new (...args: any[]): T; 13 | } 14 | 15 | type SchemaObjectFactoryModule = 16 | typeof import('@nestjs/swagger/dist/services/schema-object-factory'); 17 | 18 | export const patchNestjsSwagger = ( 19 | schemaObjectFactoryModule: SchemaObjectFactoryModule | undefined = undefined, 20 | openApiVersion: '3.0' | '3.1' = '3.0' 21 | ): void => { 22 | const { SchemaObjectFactory } = (schemaObjectFactoryModule ?? 23 | require('@nestjs/swagger/dist/services/schema-object-factory')) as SchemaObjectFactoryModule; 24 | 25 | const orgExploreModelSchema = 26 | SchemaObjectFactory.prototype.exploreModelSchema; 27 | 28 | SchemaObjectFactory.prototype.exploreModelSchema = function ( 29 | type: Type | Function | any, 30 | schemas: any | Record, 31 | schemaRefsStack: string[] = [] 32 | // type: Type | Function | any, 33 | // schemas: Record, 34 | // schemaRefsStack: string[] = [] 35 | ) { 36 | // @ts-expect-error Reported as private, but since we are patching, we will be able to reach it 37 | if (this.isLazyTypeFunc(type)) { 38 | // eslint-disable-next-line @typescript-eslint/ban-types 39 | type = (type as Function)(); 40 | } 41 | 42 | if (!type.zodSchema) { 43 | return orgExploreModelSchema.call(this, type, schemas, schemaRefsStack); 44 | } 45 | 46 | schemas[type.name] = generateSchema(type.zodSchema, false, openApiVersion); 47 | 48 | return type.name; 49 | }; 50 | }; 51 | -------------------------------------------------------------------------------- /packages/zod-nestjs/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The `Merge` and `TupleToUnion` types (and their dependant types) from the `type-fest` package. 3 | * https://github.com/sindresorhus/type-fest 4 | */ 5 | 6 | export type TupleToUnion = ArrayType extends readonly unknown[] ? ArrayType[number] : never; 7 | 8 | type PickIndexSignature = { 9 | [KeyType in keyof ObjectType as object extends Record 10 | ? KeyType 11 | : never]: ObjectType[KeyType]; 12 | }; 13 | 14 | type OmitIndexSignature = { 15 | [KeyType in keyof ObjectType as object extends Record 16 | ? never 17 | : KeyType]: ObjectType[KeyType]; 18 | }; 19 | 20 | type Simplify = {[KeyType in keyof T]: T[KeyType]}; 21 | 22 | type RequiredFilter = undefined extends Type[Key] 23 | ? Type[Key] extends undefined 24 | ? Key 25 | : never 26 | : Key; 27 | 28 | type OptionalFilter = undefined extends Type[Key] 29 | ? Type[Key] extends undefined 30 | ? never 31 | : Key 32 | : never; 33 | 34 | type EnforceOptional = Simplify<{ 35 | [Key in keyof ObjectType as RequiredFilter]: ObjectType[Key] 36 | } & { 37 | [Key in keyof ObjectType as OptionalFilter]?: Exclude 38 | }>; 39 | 40 | 41 | type SimpleMerge = { 42 | [Key in keyof Destination | keyof Source]: Key extends keyof Source 43 | ? Source[Key] 44 | : Key extends keyof Destination 45 | ? Destination[Key] 46 | : never; 47 | }; 48 | 49 | export type Merge = EnforceOptional< 50 | SimpleMerge, PickIndexSignature> 51 | & SimpleMerge, OmitIndexSignature>>; 52 | -------------------------------------------------------------------------------- /packages/zod-nestjs/src/lib/zod-validation-pipe.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was originally taken directly from: 3 | * https://github.com/kbkk/abitia/blob/master/packages/zod-dto/src/ZodValidationPipe.ts 4 | */ 5 | 6 | import { 7 | PipeTransform, 8 | Injectable, 9 | ArgumentMetadata, 10 | HttpStatus, 11 | Optional, 12 | } from '@nestjs/common'; 13 | 14 | import { ZodDtoStatic } from './create-zod-dto'; 15 | import { HTTP_ERRORS_BY_CODE } from './http-errors'; 16 | 17 | export interface ZodValidationPipeOptions { 18 | errorHttpStatusCode?: keyof typeof HTTP_ERRORS_BY_CODE; 19 | } 20 | 21 | @Injectable() 22 | export class ZodValidationPipe implements PipeTransform { 23 | private readonly errorHttpStatusCode: keyof typeof HTTP_ERRORS_BY_CODE; 24 | 25 | constructor(@Optional() options?: ZodValidationPipeOptions) { 26 | this.errorHttpStatusCode = 27 | options?.errorHttpStatusCode || HttpStatus.BAD_REQUEST; 28 | } 29 | 30 | public transform(value: unknown, metadata: ArgumentMetadata): unknown { 31 | const zodSchema = (metadata?.metatype as ZodDtoStatic)?.zodSchema; 32 | 33 | if (zodSchema) { 34 | const parseResult = zodSchema.safeParse(value); 35 | 36 | if (!parseResult.success) { 37 | const { error } = parseResult; 38 | const message = error.errors.map( 39 | (error) => `${error.path.join('.')}: ${error.message}` 40 | ); 41 | 42 | throw new HTTP_ERRORS_BY_CODE[this.errorHttpStatusCode](message); 43 | } 44 | 45 | return parseResult.data; 46 | } 47 | 48 | return value; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/zod-nestjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ], 13 | "compilerOptions": { 14 | "forceConsistentCasingInFileNames": true, 15 | "strict": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/zod-nestjs/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "../../dist/out-tsc", 6 | "declaration": true, 7 | "types": ["node"] 8 | }, 9 | "exclude": ["jest.config.ts", "**/*.spec.ts", "**/*.test.ts"], 10 | "include": ["**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/zod-nestjs/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "jest.config.ts", 10 | "**/*.test.ts", 11 | "**/*.spec.ts", 12 | "**/*.test.tsx", 13 | "**/*.spec.tsx", 14 | "**/*.test.js", 15 | "**/*.spec.js", 16 | "**/*.test.jsx", 17 | "**/*.spec.jsx", 18 | "**/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /packages/zod-openapi/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@nx/js/babel", 5 | { 6 | "useBuiltIns": "usage" 7 | } 8 | ] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /packages/zod-openapi/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/zod-openapi/LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2022 Brian McBride 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 | -------------------------------------------------------------------------------- /packages/zod-openapi/README.md: -------------------------------------------------------------------------------- 1 | # @anatine/zod-openapi 2 | 3 | Converts a [Zod](https://github.com/colinhacks/zod) schema to an OpenAPI `SchemaObject` as defined by [openapi3-ts](https://www.npmjs.com/package/openapi3-ts) 4 | 5 | ---- 6 | 7 | ## Installation 8 | 9 | Both openapi3-ts and zod are peer dependencies instead of dependant packages. 10 | While `zod` is necessary for operation, `openapi3-ts` is for type-casting. 11 | 12 | ```shell 13 | npm install openapi3-ts zod @anatine/zod-openapi 14 | ``` 15 | 16 | ---- 17 | 18 | ## Usage 19 | 20 | ### Take any Zod schema and convert it to an OpenAPI JSON object 21 | 22 | ```typescript 23 | import { generateSchema } from '@anatine/zod-openapi'; 24 | const aZodSchema = z.object({ 25 | uid: z.string().nonempty(), 26 | firstName: z.string().min(2), 27 | lastName: z.string().optional(), 28 | email: z.string().email(), 29 | phoneNumber: z.string().min(10).optional(), 30 | }) 31 | const myOpenApiSchema = generateSchema(aZodSchema); 32 | // ... 33 | ``` 34 | 35 | This will generate an OpenAPI schema for `myOpenApiSchema` 36 | 37 | ```json 38 | { 39 | "type": "object", 40 | "properties": { 41 | "uid": { 42 | "type": "string", 43 | "minLength": 1 44 | }, 45 | "firstName": { 46 | "type": "string", 47 | "minLength": 2 48 | }, 49 | "lastName": { 50 | "type": "string" 51 | }, 52 | "email": { 53 | "type": "string", 54 | "format": "email" 55 | }, 56 | "phoneNumber": { 57 | "type": "string", 58 | "minLength": 10 59 | } 60 | }, 61 | "required": [ 62 | "uid", 63 | "firstName", 64 | "email" 65 | ] 66 | } 67 | ``` 68 | 69 | ### Extend a Zod schema with additional OpenAPI schema via a function wrapper 70 | 71 | ```typescript 72 | import { extendApi, generateSchema } from '@anatine/zod-openapi'; 73 | const aZodExtendedSchema = extendApi( 74 | z.object({ 75 | uid: extendApi(z.string().nonempty(), { 76 | title: 'Unique ID', 77 | description: 'A UUID generated by the server', 78 | }), 79 | firstName: z.string().min(2), 80 | lastName: z.string().optional(), 81 | email: z.string().email(), 82 | phoneNumber: extendApi(z.string().min(10), { 83 | description: 'US Phone numbers only', 84 | example: '555-555-5555', 85 | }), 86 | }), 87 | { 88 | title: 'User', 89 | description: 'A user schema', 90 | } 91 | ); 92 | const myOpenApiSchema = generateSchema(aZodExtendedSchema); 93 | // ... 94 | ``` 95 | 96 | ... or via extension of the Zod schema: 97 | 98 | ```typescript 99 | import { extendApi, generateSchema, extendZodWithOpenApi } from '@anatine/zod-openapi'; 100 | import {z} from 'zod'; 101 | 102 | extendZodWithOpenApi(z); 103 | 104 | const aZodExtendedSchema = 105 | z.object({ 106 | uid: z.string().nonempty().openapi({ 107 | title: 'Unique ID', 108 | description: 'A UUID generated by the server', 109 | }), 110 | firstName: z.string().min(2), 111 | lastName: z.string().optional(), 112 | email: z.string().email(), 113 | phoneNumber: z.string().min(10).openapi({ 114 | description: 'US Phone numbers only', 115 | example: '555-555-5555', 116 | }), 117 | }).openapi( 118 | { 119 | title: 'User', 120 | description: 'A user schema', 121 | } 122 | ); 123 | const myOpenApiSchema = generateSchema(aZodExtendedSchema); 124 | // ... 125 | ``` 126 | 127 | This will generate an extended schema: 128 | 129 | ```json 130 | { 131 | "type": "object", 132 | "properties": { 133 | "uid": { 134 | "type": "string", 135 | "minLength": 1, 136 | "title": "Unique ID", 137 | "description": "A UUID generated by the server" 138 | }, 139 | "firstName": { 140 | "type": "string", 141 | "minLength": 2 142 | }, 143 | "lastName": { 144 | "type": "string" 145 | }, 146 | "email": { 147 | "type": "string", 148 | "format": "email" 149 | }, 150 | "phoneNumber": { 151 | "type": "string", 152 | "minLength": 10, 153 | "description": "US Phone numbers only", 154 | "example": "555-555-5555" 155 | } 156 | }, 157 | "required": [ 158 | "uid", 159 | "firstName", 160 | "email", 161 | "phoneNumber" 162 | ], 163 | "title": "User", 164 | "description": "A user schema" 165 | } 166 | ``` 167 | 168 | ---- 169 | 170 | ## Credits 171 | 172 | - ### [express-zod-api](https://github.com/RobinTail/express-zod-api) 173 | 174 | A great lib that provided some insights on dealing with various zod types. 175 | 176 | - ### [zod-dto](https://github.com/kbkk/abitia/tree/master/packages/zod-dto) 177 | 178 | Lib providing insights into using Zod with NestJS 179 | 180 | ---- 181 | 182 | This library is part of a nx monorepo [@anatine/zod-plugins](https://github.com/anatine/zod-plugins). 183 | -------------------------------------------------------------------------------- /packages/zod-openapi/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'zod-openapi', 4 | preset: '../../jest.preset.js', 5 | globals: {}, 6 | testEnvironment: 'node', 7 | transform: { 8 | '^.+\\.[tj]sx?$': [ 9 | 'ts-jest', 10 | { 11 | tsconfig: '/tsconfig.spec.json', 12 | }, 13 | ], 14 | }, 15 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 16 | coverageDirectory: '../../coverage/packages/zod-openapi', 17 | }; 18 | -------------------------------------------------------------------------------- /packages/zod-openapi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@anatine/zod-openapi", 3 | "version": "2.2.8", 4 | "description": "Zod to OpenAPI converter", 5 | "main": "src/index.js", 6 | "types": "src/index.d.ts", 7 | "license": "MIT", 8 | "public": true, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/anatine/zod-plugins" 12 | }, 13 | "homepage": "https://github.com/anatine/zod-plugins/tree/main/packages/zod-openapi", 14 | "author": { 15 | "name": "Brian McBride", 16 | "url": "https://www.linkedin.com/in/brianmcbride" 17 | }, 18 | "keywords": [ 19 | "zod", 20 | "openapi", 21 | "swagger" 22 | ], 23 | "dependencies": { 24 | "ts-deepmerge": "^6.0.3" 25 | }, 26 | "peerDependencies": { 27 | "zod": "^3.20.0", 28 | "openapi3-ts": "^4.1.2" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/zod-openapi/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zod-openapi", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "packages/zod-openapi/src", 5 | "projectType": "library", 6 | "targets": { 7 | "build": { 8 | "executor": "@nx/js:tsc", 9 | "outputs": ["{options.outputPath}"], 10 | "options": { 11 | "outputPath": "dist/packages/zod-openapi", 12 | "tsConfig": "packages/zod-openapi/tsconfig.lib.json", 13 | "packageJson": "packages/zod-openapi/package.json", 14 | "main": "packages/zod-openapi/src/index.ts", 15 | "assets": ["packages/zod-openapi/*.md"] 16 | } 17 | }, 18 | "lint": { 19 | "executor": "@nx/linter:eslint", 20 | "outputs": ["{options.outputFile}"], 21 | "options": { 22 | "lintFilePatterns": ["packages/zod-openapi/**/*.ts"] 23 | } 24 | }, 25 | "test": { 26 | "executor": "@nx/jest:jest", 27 | "outputs": ["{workspaceRoot}/coverage/packages/zod-openapi"], 28 | "options": { 29 | "jestConfig": "packages/zod-openapi/jest.config.ts", 30 | "passWithNoTests": true 31 | } 32 | }, 33 | "version": { 34 | "executor": "@jscutlery/semver:version", 35 | "preset": "conventional", 36 | "options": { 37 | "push": true, 38 | "preset": "conventional", 39 | "skipCommitTypes": ["ci"], 40 | "postTargets": [ 41 | "zod-openapi:build", 42 | "zod-openapi:publish", 43 | "zod-openapi:github" 44 | ] 45 | } 46 | }, 47 | "github": { 48 | "executor": "@jscutlery/semver:github", 49 | "options": { 50 | "tag": "${tag}", 51 | "notes": "${notes}" 52 | } 53 | }, 54 | "publish": { 55 | "executor": "ngx-deploy-npm:deploy", 56 | "options": { 57 | "access": "public", 58 | "distFolderPath": "dist/packages/zod-openapi" 59 | }, 60 | "dependsOn": ["build"] 61 | } 62 | }, 63 | "tags": [] 64 | } 65 | -------------------------------------------------------------------------------- /packages/zod-openapi/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/zod-openapi'; 2 | export * from './lib/zod-extensions'; 3 | -------------------------------------------------------------------------------- /packages/zod-openapi/src/lib/zod-extensions.spec.ts: -------------------------------------------------------------------------------- 1 | import {extendZodWithOpenApi } from "./zod-extensions"; 2 | import {z} from "zod"; 3 | import { generateSchema } from './zod-openapi'; 4 | 5 | 6 | describe('Zod Extensions', () => { 7 | 8 | it('should generate a schema for a Zod object', () => { 9 | 10 | extendZodWithOpenApi(z, true) 11 | 12 | const schema = z.object({ 13 | one: z.string().openapi({examples: ['oneOne']}), 14 | two: z.number(), 15 | three: z.number().optional(), 16 | }).openapi({ 17 | examples: [{one: 'oneOne', two: 42}], 18 | hideDefinitions: ['three'] 19 | }) 20 | 21 | const apiSchema = generateSchema(schema); 22 | 23 | expect(apiSchema).toEqual({ 24 | "examples": [{ 25 | "one": "oneOne", 26 | "two": 42 27 | }], 28 | "properties": { 29 | "one": { 30 | "examples": ["oneOne"], 31 | "type": ["string"] 32 | }, 33 | "two": { 34 | "type": ["number"] 35 | } 36 | }, 37 | "required": [ 38 | "one", 39 | "two" 40 | ], 41 | "type": ["object"], 42 | "hideDefinitions": ["three"], 43 | }) 44 | }) 45 | 46 | 47 | }) 48 | -------------------------------------------------------------------------------- /packages/zod-openapi/src/lib/zod-extensions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This code is heavily inspired by https://github.com/asteasolutions/zod-to-openapi/blob/master/src/zod-extensions.ts 3 | */ 4 | 5 | import { AnatineSchemaObject, extendApi } from './zod-openapi'; 6 | import {z} from "zod"; 7 | import {ZodTypeDef} from "zod"; 8 | 9 | 10 | declare module 'zod' { 11 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 12 | interface ZodSchema { 13 | openapi>( 14 | this: T, 15 | metadata: Partial 16 | ): T; 17 | } 18 | } 19 | 20 | export function extendZodWithOpenApi(zod: typeof z, forceOverride = false) { 21 | if (!forceOverride && typeof zod.ZodSchema.prototype.openapi !== 'undefined') { 22 | // This zod instance is already extended with the required methods, 23 | // doing it again will just result in multiple wrapper methods for 24 | // `optional` and `nullable` 25 | return; 26 | } 27 | 28 | zod.ZodSchema.prototype.openapi = function ( 29 | metadata?: Partial 30 | ) { 31 | return extendApi(this, metadata) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/zod-openapi/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ], 13 | "compilerOptions": { 14 | "forceConsistentCasingInFileNames": true, 15 | "strict": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/zod-openapi/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "../../dist/out-tsc", 6 | "declaration": true, 7 | "types": ["node"] 8 | }, 9 | "exclude": ["jest.config.ts", "**/*.spec.ts", "**/*.test.ts"], 10 | "include": ["**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/zod-openapi/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "jest.config.ts", 10 | "**/*.test.ts", 11 | "**/*.spec.ts", 12 | "**/*.test.tsx", 13 | "**/*.spec.tsx", 14 | "**/*.test.js", 15 | "**/*.spec.js", 16 | "**/*.test.jsx", 17 | "**/*.spec.jsx", 18 | "**/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tools/tsconfig.tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/tools", 5 | "rootDir": ".", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": ["node"], 9 | "importHelpers": false 10 | }, 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": false, 11 | "isolatedModules": true, 12 | "strict": true, 13 | "target": "es2017", 14 | "module": "esnext", 15 | "lib": ["es2019", "dom"], 16 | "skipLibCheck": true, 17 | "skipDefaultLibCheck": true, 18 | "baseUrl": ".", 19 | "paths": { 20 | "@anatine/graphql-codegen-zod": [ 21 | "packages/graphql-codegen-zod/src/index.ts" 22 | ], 23 | "@anatine/graphql-zod-validation": [ 24 | "packages/graphql-zod-validation/src/index.ts" 25 | ], 26 | "@anatine/zod-mock": ["packages/zod-mock/src/index.ts"], 27 | "@anatine/zod-nestjs": ["packages/zod-nestjs/src/index.ts"], 28 | "@anatine/zod-openapi": ["packages/zod-openapi/src/index.ts"] 29 | } 30 | }, 31 | "exclude": ["node_modules", "tmp"] 32 | } 33 | -------------------------------------------------------------------------------- /wallaby.js: -------------------------------------------------------------------------------- 1 | module.exports = () => ({ 2 | autoDetect: true, 3 | runMode: 'onsave' 4 | }); --------------------------------------------------------------------------------