├── .eslintrc.json ├── .github ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── ci.yml │ ├── docs.yml │ └── publish.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc.json ├── .vscode └── launch.json ├── changelog.md ├── contributing.md ├── docs ├── .gitignore ├── README.md ├── babel.config.js ├── docs │ ├── configuration │ │ ├── _category_.json │ │ ├── operations.mdx │ │ └── schema.mdx │ ├── getting-started.mdx │ └── readme.mdx ├── docusaurus.config.js ├── package.json ├── sidebars.js ├── src │ └── custom.css ├── static │ ├── .nojekyll │ └── images │ │ ├── favicon.ico │ │ └── logo.svg └── tsconfig.json ├── examples ├── minimal │ ├── codegen.yml │ ├── findUser.graphql │ ├── package.json │ ├── schema.graphql │ ├── search │ │ └── search.graphql │ └── tsconfig.json ├── usage-with-faker │ ├── codegen.yml │ ├── findUser.graphql │ ├── package.json │ ├── schema.graphql │ ├── search │ │ └── search.graphql │ └── tsconfig.json └── usage-with-near-operation-file-preset │ ├── codegen.yml │ ├── findUser.graphql │ ├── package.json │ ├── schema.graphql │ ├── search │ └── search.graphql │ └── tsconfig.json ├── license ├── package.json ├── packages └── graphql-codegen-factories │ ├── jest.config.js │ ├── package.json │ ├── src │ ├── FactoriesBaseVisitor.ts │ ├── operations │ │ ├── FactoriesOperationsVisitor.ts │ │ ├── __tests__ │ │ │ ├── __snapshots__ │ │ │ │ └── plugin.ts.snap │ │ │ └── plugin.ts │ │ ├── index.ts │ │ └── plugin.ts │ └── schema │ │ ├── FactoriesSchemaVisitor.ts │ │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ └── plugin.ts.snap │ │ └── plugin.ts │ │ ├── index.ts │ │ └── plugin.ts │ └── tsconfig.json ├── readme.md └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint", "jest"], 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/recommended", 8 | "prettier", 9 | "plugin:jest/recommended" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/packages/graphql-codegen-factories" 5 | schedule: 6 | interval: "daily" 7 | # We are only interested in security updates, 8 | # the following disables version updates: 9 | open-pull-requests-limit: 0 10 | allow: 11 | - dependency-type: "production" 12 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v2 13 | with: 14 | node-version: "16.x" 15 | registry-url: "https://registry.npmjs.org" 16 | - run: yarn 17 | - run: yarn format 18 | - run: yarn lint 19 | - run: yarn build 20 | working-directory: packages/graphql-codegen-factories 21 | - run: yarn workspaces run test 22 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | on: 3 | push: 4 | branches: [main] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-node@v2 11 | with: 12 | node-version: "16.x" 13 | registry-url: "https://registry.npmjs.org" 14 | - run: yarn 15 | working-directory: docs 16 | - run: yarn build 17 | working-directory: docs 18 | - uses: peaceiris/actions-gh-pages@v3 19 | with: 20 | github_token: ${{ secrets.GITHUB_TOKEN }} 21 | publish_dir: ./docs/build 22 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | release: 4 | types: [created] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-node@v2 11 | with: 12 | node-version: "16.x" 13 | registry-url: "https://registry.npmjs.org" 14 | - run: yarn 15 | - run: yarn publish 16 | working-directory: packages/graphql-codegen-factories 17 | env: 18 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/node,yarn 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=node,yarn 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | .pnpm-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # Snowpack dependency directory (https://snowpack.dev/) 50 | web_modules/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Optional stylelint cache 62 | .stylelintcache 63 | 64 | # Microbundle cache 65 | .rpt2_cache/ 66 | .rts2_cache_cjs/ 67 | .rts2_cache_es/ 68 | .rts2_cache_umd/ 69 | 70 | # Optional REPL history 71 | .node_repl_history 72 | 73 | # Output of 'npm pack' 74 | *.tgz 75 | 76 | # Yarn Integrity file 77 | .yarn-integrity 78 | 79 | # dotenv environment variable files 80 | .env 81 | .env.development.local 82 | .env.test.local 83 | .env.production.local 84 | .env.local 85 | 86 | # parcel-bundler cache (https://parceljs.org/) 87 | .cache 88 | .parcel-cache 89 | 90 | # Next.js build output 91 | .next 92 | out 93 | 94 | # Nuxt.js build / generate output 95 | .nuxt 96 | dist 97 | 98 | # Gatsby files 99 | .cache/ 100 | # Comment in the public line in if your project uses Gatsby and not Next.js 101 | # https://nextjs.org/blog/next-9-1#public-directory-support 102 | # public 103 | 104 | # vuepress build output 105 | .vuepress/dist 106 | 107 | # vuepress v2.x temp and cache directory 108 | .temp 109 | 110 | # Docusaurus cache and generated files 111 | .docusaurus 112 | 113 | # Serverless directories 114 | .serverless/ 115 | 116 | # FuseBox cache 117 | .fusebox/ 118 | 119 | # DynamoDB Local files 120 | .dynamodb/ 121 | 122 | # TernJS port file 123 | .tern-port 124 | 125 | # Stores VSCode versions used for testing VSCode extensions 126 | .vscode-test 127 | 128 | # yarn v2 129 | .yarn/cache 130 | .yarn/unplugged 131 | .yarn/build-state.yml 132 | .yarn/install-state.gz 133 | .pnp.* 134 | 135 | ### Node Patch ### 136 | # Serverless Webpack directories 137 | .webpack/ 138 | 139 | # Optional stylelint cache 140 | 141 | # SvelteKit build / generate output 142 | .svelte-kit 143 | 144 | ### yarn ### 145 | # https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored 146 | 147 | .yarn/* 148 | !.yarn/releases 149 | !.yarn/patches 150 | !.yarn/plugins 151 | !.yarn/sdks 152 | !.yarn/versions 153 | 154 | # if you are NOT using Zero-installs, then: 155 | # comment the following lines 156 | !.yarn/cache 157 | 158 | # and uncomment the following lines 159 | # .pnp.* 160 | 161 | # End of https://www.toptal.com/developers/gitignore/api/node,yarn 162 | 163 | build/ 164 | generated/ 165 | *.generated.ts 166 | 167 | # The package's readme is automatically copied from the root one when publishing 168 | packages/graphql-codegen-factories/readme.md 169 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/gallium -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build/ 2 | generated/ 3 | *.generated.ts 4 | .docusaurus/ 5 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug Jest Tests", 6 | "type": "node", 7 | "request": "launch", 8 | "runtimeArgs": [ 9 | "--inspect-brk", 10 | "${workspaceRoot}/node_modules/.bin/jest", 11 | "--runInBand" 12 | ], 13 | "console": "integratedTerminal", 14 | "internalConsoleOptions": "neverOpen", 15 | "port": 9229, 16 | "cwd": "${workspaceRoot}/packages/graphql-codegen-factories" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | ## 1.2.1 - 2023-03-06 6 | 7 | ### Fixed 8 | 9 | - Fix operations factories name when using a custom root query #75 10 | 11 | ## 1.2.0 - 2023-01-20 12 | 13 | ### Added 14 | 15 | - Nullable fields can now be initialized by setting the `maybeValueDefault` option to `"{defaultValue}"` 16 | 17 | ## 1.1.0 - 2023-01-19 18 | 19 | ### Added 20 | 21 | - Add `maybeValueDefault` option to customize the nullable fields' default value 22 | - Add `inputMaybeValueDefault` option to customize the nullable inputs' fields' default value 23 | - Add `disableDescriptions` option to toggle on/off objects and inputs' description added above the factory functions 24 | 25 | ### Changed 26 | 27 | - Add objects' and inputs' description above the factory functions 28 | 29 | ## 1.0.0 - 2022-05-07 30 | 31 | ### Added 32 | 33 | - Add a plugin that generates factories for operations 34 | 35 | ### Changed 36 | 37 | - Add default value for factories overrides 38 | 39 | ## 1.0.0-beta.4 - 2022-04-24 40 | 41 | ### Added 42 | 43 | - Add support for unions 44 | - Generate factories for the root types: Query, Mutation and Subscription 45 | - Generate factories for operations and each of their selections 46 | 47 | ## 1.0.0-beta.3 - 2022-04-11 48 | 49 | ### Fixed 50 | 51 | - Fix support for external fragments 52 | 53 | ## 1.0.0-beta.2 - 2022-03-27 54 | 55 | ### Fixed 56 | 57 | - Fix support for unnamed operations 58 | - Fix support for lists and nullable fields 59 | - Fix fragments support by stripping them from the output 60 | - Fix support for aliased primitive fields 61 | 62 | ## 1.0.0-beta.1 - 2022-03-26 63 | 64 | ### Added 65 | 66 | - Add the `graphql-codegen-factories/operations` entry point to generate factories for operations 67 | 68 | ### Fixed 69 | 70 | - Fix the factories output when the schema has directives 71 | 72 | ## 1.0.0-beta.0 - 2022-02-22 73 | 74 | ### Added 75 | 76 | - Add `config.typesPath` to generate the factories in a different file than the types 77 | - Add `config.importTypesNamespace` to customize the name of the import namespace 78 | 79 | ### Removed 80 | 81 | - Upgrade dependencies and drop support for Node 10 in the process 82 | 83 | ## 0.0.10 - 2021-04-08 84 | 85 | ### Added 86 | 87 | - Add support for interfaces default value generation 88 | 89 | ## 0.0.9 - 2021-04-06 90 | 91 | ### Added 92 | 93 | - Add support for unions default value generation 94 | 95 | ## 0.0.8 - 2021-01-20 96 | 97 | ### Fixed 98 | 99 | - Fix default value generation for enums that contain an underscore in their name 100 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for your interest in contributing to this project! 4 | 5 | ## Installation 6 | 7 | Here is how you can get a copy of this project running on your computer: 8 | 9 | 1. Fork the repository. 10 | 2. Clone the repository. 11 | 3. Install the dependencies by running `yarn`. 12 | 13 | This project uses yarn's workspaces so the code for the package is in `packages/graphql-codegen-factories`. 14 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | Using SSH: 30 | 31 | ``` 32 | $ USE_SSH=true yarn deploy 33 | ``` 34 | 35 | Not using SSH: 36 | 37 | ``` 38 | $ GIT_USER= yarn deploy 39 | ``` 40 | 41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 42 | -------------------------------------------------------------------------------- /docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve("@docusaurus/core/lib/babel/preset")], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/docs/configuration/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "position": 3, 3 | "label": "Configuration" 4 | } 5 | -------------------------------------------------------------------------------- /docs/docs/configuration/operations.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Operations 6 | 7 | ## `config.factoryName` 8 | 9 | By default, this plugin generates factories named `create{Type}Mock`. 10 | So for a query `GetUserQuery`, the corresponding factory will be named `createGetUserQueryMock`. 11 | 12 | ## `config.schemaFactoriesPath` 13 | 14 | By default, this plugin assumes that the operations and schema factories are generated in the same file. 15 | The operations factories reference the schema's without importing them. 16 | 17 | If they are not generated in the same file, you need to provide the `schemaFactoriesPath`. 18 | 19 | ## `config.namespacedSchemaFactoriesImportName` 20 | 21 | By default, the import factories namespace when using `config.schemaFactories` is `schemaFactories`. 22 | You can customize this namespace by configuring `namespacedSchemaFactoriesImportName`. 23 | -------------------------------------------------------------------------------- /docs/docs/configuration/schema.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Schema 6 | 7 | ## `config.factoryName` 8 | 9 | By default, this plugin generates factories named `create{Type}Mock`. 10 | So for a type `User`, the corresponding factory will be named `createUserMock`. 11 | 12 |
13 | Example 14 | 15 |
16 | 17 | ```graphql title="schema.graphql" 18 | type User { 19 | id: ID! 20 | username: String! 21 | } 22 | ``` 23 | 24 | ```typescript title="types.ts" 25 | // highlight-start 26 | export function createUserMock(props: Partial): User { 27 | // highlight-end 28 | return { 29 | id: "", 30 | username: "", 31 | ...props, 32 | }; 33 | } 34 | ``` 35 | 36 |
37 | 38 |
39 | 40 | You can customize the factories' name by configuring `factoryName`: 41 | 42 | ```yml title="codegen.yml" 43 | overwrite: true 44 | schema: ./schema.graphql 45 | generates: 46 | ./types.ts: 47 | plugins: 48 | - typescript 49 | - graphql-codegen-factories/schema 50 | config: 51 | # highlight-start 52 | factoryName: new{Type} 53 | # highlight-end 54 | ``` 55 | 56 |
57 | Example 58 | 59 |
60 | 61 | ```graphql title="schema.graphql" 62 | type User { 63 | id: ID! 64 | username: String! 65 | } 66 | ``` 67 | 68 | ```typescript title="types.ts" 69 | // highlight-start 70 | export function newUser(props: Partial): User { 71 | // highlight-end 72 | return { 73 | id: "", 74 | username: "", 75 | ...props, 76 | }; 77 | } 78 | ``` 79 | 80 |
81 | 82 |
83 | 84 | ## `config.scalarDefaults` 85 | 86 | By default, this plugin infers the default values based on the properties' type. 87 | For example, a property whose type is `Boolean` will have a value of `false`. 88 | 89 |
90 | Example 91 | 92 |
93 | 94 | ```graphql title="schema.graphql" 95 | type User { 96 | isAdmin: Boolean! 97 | } 98 | ``` 99 | 100 | ```typescript title="types.ts" 101 | export function createUserMock(props: Partial): User { 102 | return { 103 | // highlight-start 104 | isAdmin: false, 105 | // highlight-end 106 | ...props, 107 | }; 108 | } 109 | ``` 110 | 111 |
112 | 113 |
114 | 115 | You can customize the default values by configuring `scalarDefaults`: 116 | 117 | ```yml title="codegen.yml" 118 | overwrite: true 119 | schema: ./schema.graphql 120 | generates: 121 | ./types.ts: 122 | plugins: 123 | - typescript 124 | - graphql-codegen-factories/schema 125 | config: 126 | scalarDefaults: 127 | # highlight-start 128 | Boolean: true 129 | # highlight-end 130 | ``` 131 | 132 |
133 | Example 134 | 135 |
136 | 137 | ```graphql title="schema.graphql" 138 | type User { 139 | isAdmin: Boolean! 140 | } 141 | ``` 142 | 143 | ```typescript title="types.ts" 144 | export function createUserMock(props: Partial): User { 145 | return { 146 | // highlight-start 147 | isAdmin: true, 148 | // highlight-end 149 | ...props, 150 | }; 151 | } 152 | ``` 153 | 154 |
155 | 156 |
157 | 158 | :::caution 159 | 160 | This plugin only infers default values for built-in scalars. 161 | You will probably need to use this option to define the values of custom scalars, e.g `Date`. 162 | 163 |
164 | Example 165 | 166 | ```yml title="codegen.yml" 167 | overwrite: true 168 | schema: ./schema.graphql 169 | generates: 170 | ./types.ts: 171 | plugins: 172 | - typescript 173 | - graphql-codegen-factories/schema 174 | config: 175 | scalarDefaults: 176 | # highlight-start 177 | Date: new Date() 178 | # highlight-end 179 | ``` 180 | 181 |
182 | 183 | ```graphql title="schema.graphql" 184 | scalar Date 185 | 186 | type User { 187 | createdAt: Date! 188 | } 189 | ``` 190 | 191 | ```typescript title="types.ts" 192 | export function createUserMock(props: Partial): User { 193 | return { 194 | // highlight-start 195 | createdAt: new Date(), 196 | // highlight-end 197 | ...props, 198 | }; 199 | } 200 | ``` 201 | 202 |
203 | 204 |
205 | 206 | ::: 207 | 208 | ## `config.typesPath` 209 | 210 | By default, this plugin assumes that the types and factories are generated in the same file. 211 | The factories reference types without importing them. 212 | 213 | If you want to generate types and factories in different files, you need to provide the `typesPath`: 214 | 215 | ```yml title="codegen.yml" 216 | overwrite: true 217 | schema: ./schema.graphql 218 | generates: 219 | ./types.ts: 220 | plugins: 221 | - typescript 222 | ./factories.ts: 223 | plugins: 224 | - graphql-codegen-factories/schema 225 | config: 226 | # highlight-start 227 | typesPath: ./types 228 | # highlight-end 229 | ``` 230 | 231 |
232 | Example 233 |
234 | 235 | ```graphql title="schema.graphql" 236 | type User { 237 | id: ID! 238 | username: String! 239 | } 240 | ``` 241 | 242 | ```typescript title="factories.ts" 243 | // highlight-start 244 | import * as Types from "./types"; 245 | // highlight-end 246 | 247 | export function createUserMock(props: Partial): Types.User { 248 | return { 249 | id: "", 250 | username: "", 251 | ...props, 252 | }; 253 | } 254 | ``` 255 | 256 |
257 | 258 |
259 | 260 | :::info 261 | 262 | You don't need to configure this option when using [@graphql-codegen/near-operation-file-preset](https://www.graphql-code-generator.com/plugins/near-operation-file-preset). 263 | 264 | ::: 265 | 266 | ## `config.importTypesNamespace` 267 | 268 | By default, the import types namespace when using `config.typesPath` is `Types`. 269 | 270 | You can customize this namespace by configuring `importTypesNamespace`: 271 | 272 | ```yml 273 | overwrite: true 274 | schema: ./schema.graphql 275 | generates: 276 | ./types.ts: 277 | plugins: 278 | - typescript 279 | ./factories.ts: 280 | plugins: 281 | - graphql-codegen-factories/schema 282 | config: 283 | typesPath: ./types 284 | # highlight-start 285 | importTypesNamespace: SharedTypes 286 | # highlight-end 287 | ``` 288 | 289 |
290 | Example 291 |
292 | 293 | ```graphql title="schema.graphql" 294 | type User { 295 | id: ID! 296 | username: String! 297 | } 298 | ``` 299 | 300 | ```typescript title="factories.ts" 301 | // highlight-start 302 | import * as SharedTypes from "./types"; 303 | // highlight-end 304 | 305 | export function createUserMock( 306 | props: Partial 307 | ): SharedTypes.User { 308 | return { 309 | id: "", 310 | username: "", 311 | ...props, 312 | }; 313 | } 314 | ``` 315 | 316 |
317 | 318 |
319 | 320 | ## `config.maybeValueDefault` 321 | 322 | By default, nullable fields are initialized as "null". 323 | 324 | You can customize this default value through `maybeValueDefault`: 325 | 326 | ```yml 327 | overwrite: true 328 | schema: ./schema.graphql 329 | generates: 330 | ./types.ts: 331 | plugins: 332 | - typescript 333 | - graphql-codegen-factories/schema 334 | config: 335 | # highlight-start 336 | maybeValueDefault: undefined 337 | # highlight-end 338 | ``` 339 | 340 |
341 | Example 342 | 343 |
344 | 345 | ```graphql title="schema.graphql" 346 | type Post { 347 | title: String 348 | } 349 | 350 | input PostInput { 351 | title: String 352 | } 353 | ``` 354 | 355 | ```typescript title="types.ts" 356 | export function createPost(props: Partial = {}): Post { 357 | return { 358 | // highlight-start 359 | title: undefined, 360 | // highlight-end 361 | ...props, 362 | }; 363 | } 364 | 365 | export function createPostInputMock(props: Partial = {}): PostInput { 366 | return { 367 | // highlight-start 368 | title: undefined, 369 | // highlight-end 370 | ...props, 371 | }; 372 | } 373 | ``` 374 | 375 |
376 |
377 | 378 | This option can also be used to initialize nullable fields, instead of defaulting to a static value: 379 | 380 | ```yml 381 | overwrite: true 382 | schema: ./schema.graphql 383 | generates: 384 | ./types.ts: 385 | plugins: 386 | - typescript 387 | - graphql-codegen-factories/schema 388 | config: 389 | # highlight-start 390 | maybeValueDefault: "{defaultValue}" 391 | # highlight-end 392 | ``` 393 | 394 |
395 | Example 396 | 397 |
398 | 399 | ```graphql title="schema.graphql" 400 | type Post { 401 | author: PostAuthor 402 | } 403 | 404 | type PostAuthor { 405 | username: String 406 | } 407 | ``` 408 | 409 | ```typescript title="types.ts" 410 | export function createPost(props: Partial = {}): Post { 411 | return { 412 | // highlight-start 413 | author: createPostAuthor(), 414 | // highlight-end 415 | ...props, 416 | }; 417 | } 418 | 419 | export function createPostAuthor(props: Partial = {}): Post { 420 | return { 421 | // highlight-start 422 | username: "", 423 | // highlight-end 424 | ...props, 425 | }; 426 | } 427 | ``` 428 | 429 |
430 |
431 | 432 | ## `config.inputMaybeValueDefault` 433 | 434 | By default, inputs' nullable fields are initialized as `config.maybeValueDefault` ("null" by default). 435 | 436 | You can customize this default value through `inputMaybeValueDefault`: 437 | 438 | ```yml 439 | overwrite: true 440 | schema: ./schema.graphql 441 | generates: 442 | ./types.ts: 443 | plugins: 444 | - typescript 445 | - graphql-codegen-factories/schema 446 | config: 447 | # highlight-start 448 | inputMaybeValueDefault: undefined 449 | # highlight-end 450 | ``` 451 | 452 |
453 | Example 454 | 455 |
456 | 457 | ```graphql title="schema.graphql" 458 | input PostInput { 459 | title: String 460 | } 461 | ``` 462 | 463 | ```typescript title="types.ts" 464 | export function createPostInputMock(props: Partial = {}): PostInput { 465 | return { 466 | // highlight-start 467 | title: undefined, 468 | // highlight-end 469 | ...props, 470 | }; 471 | } 472 | ``` 473 | 474 |
475 |
476 | 477 | See `config.maybeValueDefault` to initialize nullable input fields by setting `config.inputMaybeValueDefault` to `"{defaultValue}"`. 478 | 479 | ## `config.disableDescriptions` 480 | 481 | By default, objects' and inputs' description is added above the factory function. 482 | 483 | You can turn it off by setting `disableDescriptions` to `true`: 484 | 485 | ```yml 486 | overwrite: true 487 | schema: ./schema.graphql 488 | generates: 489 | ./types.ts: 490 | plugins: 491 | - typescript 492 | - graphql-codegen-factories/schema 493 | config: 494 | # highlight-start 495 | disableDescriptions: true 496 | # highlight-end 497 | ``` 498 | 499 |
500 | Example 501 | 502 |
503 | 504 | ```graphql title="schema.graphql" 505 | """ 506 | Description of a Post object. 507 | """ 508 | type Post { 509 | title: String 510 | } 511 | ``` 512 | 513 | ```typescript title="types.ts" 514 | export function createPostInputMock(props: Partial = {}): PostInput { 515 | return { 516 | title: undefined, 517 | ...props, 518 | }; 519 | } 520 | ``` 521 | 522 |
523 |
524 | -------------------------------------------------------------------------------- /docs/docs/getting-started.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | import Tabs from "@theme/Tabs"; 6 | import TabItem from "@theme/TabItem"; 7 | 8 | # Getting Started 9 | 10 | ## Installation 11 | 12 | 13 | 14 | 15 | ``` 16 | yarn add --dev graphql-codegen-factories 17 | ``` 18 | 19 | 20 | 21 | 22 | ```sh 23 | npm install --save-dev graphql-codegen-factories 24 | ``` 25 | 26 | 27 | 28 | 29 | The package is split into two plugins: 30 | 31 | - `graphql-codegen-factories/schema`: generates factories based on a GraphQL schema. 32 | - `graphql-codegen-factories/operations`: generates factories based on GraphQL operations. The schema plugin is required for the operations plugin to work as it leverages the factories generated by the former. 33 | 34 | ## Schema 35 | 36 | The factories generated by this plugin use the types generated by [@graphql-codegen/typescript](https://www.graphql-code-generator.com/plugins/typescript) so make sure it is installed. 37 | The next step is to add `graphql-codegen-factories/schema` to the list of plugins in your `codegen.yml` configuration file. 38 | 39 | ```yml title="codegen.yml" 40 | overwrite: true 41 | schema: ./schema.graphql 42 | documents: ./**/*.graphql 43 | generates: 44 | ./types.ts: 45 | plugins: 46 | - typescript 47 | # highlight-start 48 | - graphql-codegen-factories/schema 49 | # highlight-end 50 | ``` 51 | 52 | To access the full list of options, see the documentation for the [configuration](./configuration/schema.mdx). 53 | 54 | ## Operations 55 | 56 | The factories generated by this plugin use the types generated by [@graphql-codegen/typescript-operations](https://www.graphql-code-generator.com/plugins/typescript-operations) so make sure it is installed. 57 | It also leverages the factories generated by the schema plugin which must be installed as described above. 58 | The next step is to add `graphql-codegen-factories/operations` to the list of plugins in your `codegen.yml` configuration file along with the path to the file with the factories. 59 | 60 | ```yml title="codegen.yml" 61 | overwrite: true 62 | schema: ./schema.graphql 63 | documents: ./**/*.graphql 64 | generates: 65 | ./types.ts: 66 | plugins: 67 | - typescript 68 | - graphql-codegen-factories/schema 69 | - typescript-operations 70 | # highlight-start 71 | - graphql-codegen-factories/operations 72 | # highlight-end 73 | ``` 74 | 75 | To access the full list of options, see the documentation for the [configuration](./configuration/operations.mdx). 76 | -------------------------------------------------------------------------------- /docs/docs/readme.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Introduction 6 | 7 | `graphql-codegen-factories` is a plugin for [GraphQL Code Generator](https://www.graphql-code-generator.com/) that generates factories from a GraphQL schema and operations. 8 | The factories can then be used to mock data, e.g for testing or seeding a database. 9 | 10 | For example, given this GraphQL schema: 11 | 12 | ```graphql 13 | type User { 14 | id: ID! 15 | username: String! 16 | } 17 | ``` 18 | 19 | The following factory will be generated: 20 | 21 | ```typescript 22 | export type User = /* generated by @graphql-codegen/typescript */; 23 | 24 | export function createUserMock(props: Partial = {}): User { 25 | return { 26 | __typename: "User", 27 | id: "", 28 | username: "", 29 | ...props, 30 | }; 31 | } 32 | ``` 33 | 34 | It is also possible to generate factories from an operation, for example: 35 | 36 | ```graphql 37 | query GetUser { 38 | user { 39 | id 40 | username 41 | } 42 | } 43 | ``` 44 | 45 | Will result in the following factories: 46 | 47 | ```typescript 48 | export type GetUserQuery = /* generated by @graphql-codegen/typescript-operations */; 49 | 50 | export function createGetUserQueryMock(props: Partial = {}): GetUserQuery { 51 | return { 52 | __typename: "Query", 53 | user: createGetUserQueryMock_user({}), 54 | ...props, 55 | }; 56 | } 57 | 58 | export function createGetUserQueryMock_user(props: Partial = {}): GetUserQuery["user"] { 59 | return { 60 | __typename: "User", 61 | id: "", 62 | username: "", 63 | ...props, 64 | }; 65 | } 66 | ``` 67 | 68 | You can also use a fake data generator to generate realistic factories such as: 69 | 70 | ```typescript 71 | import { faker } from "@faker-js/faker"; 72 | 73 | export function createUserMock(props: Partial = {}): User { 74 | return { 75 | __typename: "User", 76 | id: faker.random.alphaNumeric(16), 77 | username: faker.lorem.word(), 78 | ...props, 79 | }; 80 | } 81 | ``` 82 | 83 | - [Documentation](https://gabinaureche.com/graphql-codegen-factories/) 84 | - [Examples](https://github.com/zhouzi/graphql-codegen-factories/tree/main/examples) 85 | - [Minimal](https://stackblitz.com/github/zhouzi/graphql-codegen-factories/tree/main/examples/minimal) 86 | - [Usage with near-operation-file-preset](https://stackblitz.com/github/zhouzi/graphql-codegen-factories/tree/main/examples/usage-with-near-operation-file-preset) 87 | - [Usage with faker](https://stackblitz.com/github/zhouzi/graphql-codegen-factories/tree/main/examples/usage-with-faker) 88 | 89 | ## Showcase 90 | 91 | - [yummy-recipes/yummy](https://github.com/yummy-recipes/yummy) 92 | 93 | Are you using this plugin? [Let us know!](https://github.com/zhouzi/graphql-codegen-factories/issues/new) 94 | -------------------------------------------------------------------------------- /docs/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Note: type annotations allow type checking and IDEs autocompletion 3 | 4 | const lightCodeTheme = require("prism-react-renderer/themes/github"); 5 | const darkCodeTheme = require("prism-react-renderer/themes/vsDark"); 6 | const packageJson = require("../packages/graphql-codegen-factories/package.json"); 7 | 8 | /** @type {import('@docusaurus/types').Config} */ 9 | const config = { 10 | title: packageJson.name, 11 | tagline: packageJson.description, 12 | url: "https://gabinaureche.com/", 13 | baseUrl: "/graphql-codegen-factories/", 14 | onBrokenLinks: "throw", 15 | onBrokenMarkdownLinks: "warn", 16 | favicon: "images/favicon.ico", 17 | organizationName: "zhouzi", 18 | projectName: packageJson.name, 19 | 20 | presets: [ 21 | [ 22 | "classic", 23 | /** @type {import('@docusaurus/preset-classic').Options} */ 24 | ({ 25 | theme: { 26 | customCss: [require.resolve("./src/custom.css")], 27 | }, 28 | docs: { 29 | routeBasePath: "/", 30 | sidebarPath: require.resolve("./sidebars.js"), 31 | // Please change this to your repo. 32 | editUrl: `https://github.com/zhouzi/${packageJson.name}/blob/main/docs/`, 33 | }, 34 | blog: false, 35 | }), 36 | ], 37 | ], 38 | 39 | themeConfig: 40 | /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ 41 | ({ 42 | navbar: { 43 | title: packageJson.name, 44 | logo: { 45 | alt: packageJson.name, 46 | src: "images/logo.svg", 47 | }, 48 | items: [ 49 | { 50 | href: `https://github.com/zhouzi/${packageJson.name}`, 51 | label: "GitHub", 52 | position: "right", 53 | }, 54 | { 55 | href: `https://www.npmjs.com/package/${packageJson.name}`, 56 | label: "npm", 57 | position: "right", 58 | }, 59 | ], 60 | }, 61 | prism: { 62 | theme: lightCodeTheme, 63 | darkTheme: darkCodeTheme, 64 | }, 65 | }), 66 | }; 67 | 68 | module.exports = config; 69 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc", 16 | "test": "yarn build" 17 | }, 18 | "dependencies": { 19 | "@docusaurus/core": "2.0.0-beta.18", 20 | "@docusaurus/preset-classic": "2.0.0-beta.18", 21 | "@mdx-js/react": "^1.6.22", 22 | "clsx": "^1.1.1", 23 | "prism-react-renderer": "^1.3.1", 24 | "react": "^17.0.2", 25 | "react-dom": "^17.0.2" 26 | }, 27 | "devDependencies": { 28 | "@docusaurus/module-type-aliases": "2.0.0-beta.18", 29 | "@tsconfig/docusaurus": "^1.0.5", 30 | "typescript": "^4.6.3" 31 | }, 32 | "browserslist": { 33 | "production": [ 34 | ">0.5%", 35 | "not dead", 36 | "not op_mini all" 37 | ], 38 | "development": [ 39 | "last 1 chrome version", 40 | "last 1 firefox version", 41 | "last 1 safari version" 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /docs/sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creating a sidebar enables you to: 3 | - create an ordered group of docs 4 | - render a sidebar for each doc of that group 5 | - provide next/previous navigation 6 | 7 | The sidebars can be generated from the filesystem, or explicitly defined here. 8 | 9 | Create as many sidebars as you want. 10 | */ 11 | 12 | // @ts-check 13 | 14 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ 15 | const sidebars = { 16 | // By default, Docusaurus generates a sidebar from the docs folder structure 17 | default: [ 18 | { type: "autogenerated", dirName: "." }, 19 | { 20 | type: "link", 21 | label: "Changelog", 22 | href: "https://github.com/zhouzi/graphql-codegen-factories/blob/main/changelog.md", 23 | }, 24 | ], 25 | 26 | // But you can create a sidebar manually 27 | /* 28 | tutorialSidebar: [ 29 | { 30 | type: 'category', 31 | label: 'Tutorial', 32 | items: ['hello'], 33 | }, 34 | ], 35 | */ 36 | }; 37 | 38 | module.exports = sidebars; 39 | -------------------------------------------------------------------------------- /docs/src/custom.css: -------------------------------------------------------------------------------- 1 | .docusaurus-highlight-code-line { 2 | display: block; 3 | background-color: rgba(0, 0, 0, 0.1); 4 | margin: 0 calc(-1 * var(--ifm-pre-padding)); 5 | padding: 0 var(--ifm-pre-padding); 6 | } 7 | [data-theme="dark"] .docusaurus-highlight-code-line { 8 | background-color: rgba(255, 255, 255, 0.1); 9 | } 10 | 11 | .codeBlocks { 12 | display: flex; 13 | } 14 | .codeBlocks > * { 15 | flex: 1; 16 | display: flex; 17 | flex-direction: column; 18 | } 19 | .codeBlocks > * > *:last-child { 20 | flex: 1; 21 | } 22 | -------------------------------------------------------------------------------- /docs/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhouzi/graphql-codegen-factories/3f049bc030401dd8b08e3da31a37a0d9bf10bef4/docs/static/.nojekyll -------------------------------------------------------------------------------- /docs/static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhouzi/graphql-codegen-factories/3f049bc030401dd8b08e3da31a37a0d9bf10bef4/docs/static/images/favicon.ico -------------------------------------------------------------------------------- /docs/static/images/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@tsconfig/docusaurus/tsconfig.json", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/minimal/codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: ./schema.graphql 3 | documents: ./**/*.graphql 4 | generates: 5 | ./generated/types.ts: 6 | plugins: 7 | - typescript 8 | - graphql-codegen-factories/schema 9 | - typescript-operations 10 | - graphql-codegen-factories/operations 11 | config: 12 | scalarDefaults: 13 | Date: new Date() 14 | -------------------------------------------------------------------------------- /examples/minimal/findUser.graphql: -------------------------------------------------------------------------------- 1 | query findUser($userId: ID!) { 2 | user(id: $userId) { 3 | ...UserFields 4 | } 5 | } 6 | 7 | fragment UserFields on User { 8 | id 9 | username 10 | role 11 | } 12 | -------------------------------------------------------------------------------- /examples/minimal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "minimal", 4 | "version": "0.0.0", 5 | "scripts": { 6 | "start": "graphql-codegen --watch", 7 | "build": "graphql-codegen", 8 | "test": "yarn build", 9 | "posttest": "tsc" 10 | }, 11 | "devDependencies": { 12 | "@graphql-codegen/cli": "^2.6.2", 13 | "@graphql-codegen/typescript": "^2.4.8", 14 | "@graphql-codegen/typescript-operations": "^2.3.7", 15 | "@tsconfig/recommended": "^1.0.1", 16 | "graphql": "^16.3.0", 17 | "graphql-codegen-factories": "1.2.1", 18 | "typescript": "^4.6.3" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/minimal/schema.graphql: -------------------------------------------------------------------------------- 1 | scalar Date 2 | 3 | schema { 4 | query: Query 5 | } 6 | 7 | type Query { 8 | me: User! 9 | user(id: ID!): User 10 | allUsers: [User] 11 | search(term: String!): [SearchResult!]! 12 | myChats: [Chat!]! 13 | } 14 | 15 | enum Role { 16 | USER 17 | ADMIN 18 | } 19 | 20 | interface Node { 21 | id: ID! 22 | } 23 | 24 | union SearchResult = User | Chat | ChatMessage 25 | 26 | type User implements Node { 27 | id: ID! 28 | username: String! 29 | email: String! 30 | role: Role! 31 | } 32 | 33 | type Chat implements Node { 34 | id: ID! 35 | users: [User!]! 36 | messages: [ChatMessage!]! 37 | } 38 | 39 | type ChatMessage implements Node { 40 | id: ID! 41 | content: String! 42 | time: Date! 43 | user: User! 44 | } 45 | -------------------------------------------------------------------------------- /examples/minimal/search/search.graphql: -------------------------------------------------------------------------------- 1 | query search($term: String!) { 2 | search(term: $term) { 3 | ... on Node { 4 | id 5 | } 6 | ... on User { 7 | fullname: username 8 | } 9 | ... on Chat { 10 | users { 11 | id 12 | } 13 | } 14 | ... on ChatMessage { 15 | ...ChatMessageFields 16 | } 17 | } 18 | } 19 | 20 | fragment ChatMessageFields on ChatMessage { 21 | content 22 | user { 23 | email 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/minimal/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/recommended/tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/usage-with-faker/codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: ./schema.graphql 3 | generates: 4 | ./generated/types.ts: 5 | plugins: 6 | - add: 7 | content: 'import { faker } from "@faker-js/faker";' 8 | - typescript 9 | - graphql-codegen-factories/schema 10 | config: 11 | scalarDefaults: 12 | ID: "faker.random.alphaNumeric(16)" 13 | String: "faker.lorem.word()" 14 | Date: "faker.date.past(1)" 15 | -------------------------------------------------------------------------------- /examples/usage-with-faker/findUser.graphql: -------------------------------------------------------------------------------- 1 | query findUser($userId: ID!) { 2 | user(id: $userId) { 3 | ...UserFields 4 | } 5 | } 6 | 7 | fragment UserFields on User { 8 | id 9 | username 10 | role 11 | } 12 | -------------------------------------------------------------------------------- /examples/usage-with-faker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "usage-with-faker", 4 | "version": "0.0.0", 5 | "scripts": { 6 | "start": "graphql-codegen --watch", 7 | "build": "graphql-codegen", 8 | "test": "yarn build", 9 | "posttest": "tsc" 10 | }, 11 | "devDependencies": { 12 | "@faker-js/faker": "^6.2.0", 13 | "@graphql-codegen/add": "^3.1.1", 14 | "@graphql-codegen/cli": "^2.6.2", 15 | "@graphql-codegen/typescript": "^2.4.8", 16 | "@tsconfig/recommended": "^1.0.1", 17 | "graphql": "^16.3.0", 18 | "graphql-codegen-factories": "1.2.1", 19 | "typescript": "^4.6.3" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/usage-with-faker/schema.graphql: -------------------------------------------------------------------------------- 1 | scalar Date 2 | 3 | schema { 4 | query: Query 5 | } 6 | 7 | type Query { 8 | me: User! 9 | user(id: ID!): User 10 | allUsers: [User] 11 | search(term: String!): [SearchResult!]! 12 | myChats: [Chat!]! 13 | } 14 | 15 | enum Role { 16 | USER 17 | ADMIN 18 | } 19 | 20 | interface Node { 21 | id: ID! 22 | } 23 | 24 | union SearchResult = User | Chat | ChatMessage 25 | 26 | type User implements Node { 27 | id: ID! 28 | username: String! 29 | email: String! 30 | role: Role! 31 | } 32 | 33 | type Chat implements Node { 34 | id: ID! 35 | users: [User!]! 36 | messages: [ChatMessage!]! 37 | } 38 | 39 | type ChatMessage implements Node { 40 | id: ID! 41 | content: String! 42 | time: Date! 43 | user: User! 44 | } 45 | -------------------------------------------------------------------------------- /examples/usage-with-faker/search/search.graphql: -------------------------------------------------------------------------------- 1 | query search($term: String!) { 2 | search(term: $term) { 3 | ... on Node { 4 | id 5 | } 6 | ... on User { 7 | fullname: username 8 | } 9 | ... on Chat { 10 | users { 11 | id 12 | } 13 | } 14 | ... on ChatMessage { 15 | ...ChatMessageFields 16 | } 17 | } 18 | } 19 | 20 | fragment ChatMessageFields on ChatMessage { 21 | content 22 | user { 23 | email 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/usage-with-faker/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/recommended/tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/usage-with-near-operation-file-preset/codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: ./schema.graphql 3 | documents: ./**/*.graphql 4 | generates: 5 | ./generated/types.ts: 6 | plugins: 7 | - typescript 8 | ./generated/factories.ts: 9 | plugins: 10 | - graphql-codegen-factories/schema 11 | config: 12 | typesPath: ./types 13 | scalarDefaults: 14 | Date: new Date() 15 | ./: 16 | preset: near-operation-file 17 | presetConfig: 18 | extension: .generated.ts 19 | baseTypesPath: ./generated/types.ts 20 | config: 21 | schemaFactoriesPath: ./generated/factories.ts 22 | plugins: 23 | - typescript-operations 24 | - graphql-codegen-factories/operations 25 | -------------------------------------------------------------------------------- /examples/usage-with-near-operation-file-preset/findUser.graphql: -------------------------------------------------------------------------------- 1 | query findUser($userId: ID!) { 2 | user(id: $userId) { 3 | ...UserFields 4 | } 5 | } 6 | 7 | fragment UserFields on User { 8 | id 9 | username 10 | role 11 | } 12 | -------------------------------------------------------------------------------- /examples/usage-with-near-operation-file-preset/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "usage-with-near-operation-file-preset", 4 | "version": "0.0.0", 5 | "scripts": { 6 | "start": "graphql-codegen --watch", 7 | "build": "graphql-codegen", 8 | "test": "yarn build", 9 | "posttest": "tsc" 10 | }, 11 | "devDependencies": { 12 | "@graphql-codegen/cli": "^2.6.2", 13 | "@graphql-codegen/near-operation-file-preset": "^2.2.9", 14 | "@graphql-codegen/typescript": "^2.4.8", 15 | "@graphql-codegen/typescript-operations": "^2.3.5", 16 | "@tsconfig/recommended": "^1.0.1", 17 | "graphql": "^16.3.0", 18 | "graphql-codegen-factories": "1.2.1", 19 | "typescript": "^4.6.3" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/usage-with-near-operation-file-preset/schema.graphql: -------------------------------------------------------------------------------- 1 | scalar Date 2 | 3 | schema { 4 | query: Query 5 | } 6 | 7 | type Query { 8 | me: User! 9 | user(id: ID!): User 10 | allUsers: [User] 11 | search(term: String!): [SearchResult!]! 12 | myChats: [Chat!]! 13 | } 14 | 15 | enum Role { 16 | USER 17 | ADMIN 18 | } 19 | 20 | interface Node { 21 | id: ID! 22 | } 23 | 24 | union SearchResult = User | Chat | ChatMessage 25 | 26 | type User implements Node { 27 | id: ID! 28 | username: String! 29 | email: String! 30 | role: Role! 31 | } 32 | 33 | type Chat implements Node { 34 | id: ID! 35 | users: [User!]! 36 | messages: [ChatMessage!]! 37 | } 38 | 39 | type ChatMessage implements Node { 40 | id: ID! 41 | content: String! 42 | time: Date! 43 | user: User! 44 | } 45 | -------------------------------------------------------------------------------- /examples/usage-with-near-operation-file-preset/search/search.graphql: -------------------------------------------------------------------------------- 1 | query search($term: String!) { 2 | search(term: $term) { 3 | ... on Node { 4 | id 5 | } 6 | ... on User { 7 | fullname: username 8 | } 9 | ... on Chat { 10 | users { 11 | id 12 | } 13 | } 14 | ... on ChatMessage { 15 | ...ChatMessageFields 16 | } 17 | } 18 | } 19 | 20 | fragment ChatMessageFields on ChatMessage { 21 | content 22 | user { 23 | email 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/usage-with-near-operation-file-preset/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/recommended/tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-Present Gabin Aureche 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. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "workspaces": [ 4 | "docs", 5 | "examples/*", 6 | "packages/*" 7 | ], 8 | "scripts": { 9 | "format": "prettier --check .", 10 | "lint": "eslint --ext .ts,.tsx ." 11 | }, 12 | "devDependencies": { 13 | "@typescript-eslint/eslint-plugin": "^5.16.0", 14 | "@typescript-eslint/parser": "^5.16.0", 15 | "eslint": "^8.12.0", 16 | "eslint-config-prettier": "^8.5.0", 17 | "eslint-plugin-jest": "^26.1.5", 18 | "prettier": "^2.6.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/graphql-codegen-factories/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | testPathIgnorePatterns: ["build"], 5 | }; 6 | -------------------------------------------------------------------------------- /packages/graphql-codegen-factories/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-codegen-factories", 3 | "version": "1.2.1", 4 | "description": "graphql-codegen plugin to generate factories", 5 | "keywords": [ 6 | "graphql", 7 | "graphql-codegen", 8 | "plugin", 9 | "factories" 10 | ], 11 | "homepage": "https://github.com/zhouzi/graphql-codegen-factories#readme", 12 | "bugs": { 13 | "url": "https://github.com/zhouzi/graphql-codegen-factories/issues", 14 | "email": "hello@gabinaureche.com" 15 | }, 16 | "license": "MIT", 17 | "files": [ 18 | "build" 19 | ], 20 | "main": "./build/schema/index.js", 21 | "exports": { 22 | ".": "./build/schema/index.js", 23 | "./schema": "./build/schema/index.js", 24 | "./operations": "./build/operations/index.js" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/zhouzi/graphql-codegen-factories.git" 29 | }, 30 | "devDependencies": { 31 | "@tsconfig/recommended": "^1.0.1", 32 | "@types/jest": "^27.4.1", 33 | "graphql": "^16.3.0", 34 | "jest": "^27.5.1", 35 | "ts-jest": "^27.1.4", 36 | "typescript": "^4.6.3" 37 | }, 38 | "peerDependencies": { 39 | "graphql": "^16.3.0" 40 | }, 41 | "dependencies": { 42 | "@graphql-codegen/plugin-helpers": "^2.4.2", 43 | "@graphql-codegen/visitor-plugin-common": "^2.7.4", 44 | "change-case-all": "^1.0.14" 45 | }, 46 | "scripts": { 47 | "prebuild": "rm -rf ./build", 48 | "build": "tsc", 49 | "postbuild": "cp ../../readme.md .", 50 | "test": "jest", 51 | "prepare": "yarn build" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/graphql-codegen-factories/src/FactoriesBaseVisitor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseVisitor, 3 | getConfigValue, 4 | indent, 5 | ParsedTypesConfig, 6 | RawTypesConfig, 7 | } from "@graphql-codegen/visitor-plugin-common"; 8 | 9 | export interface FactoriesBaseVisitorRawConfig extends RawTypesConfig { 10 | factoryName?: string; 11 | } 12 | 13 | export interface FactoriesBaseVisitorParsedConfig extends ParsedTypesConfig { 14 | factoryName: string; 15 | } 16 | 17 | type PrintLines = Array; 18 | 19 | export class FactoriesBaseVisitor< 20 | RawConfig extends FactoriesBaseVisitorRawConfig, 21 | ParsedConfig extends FactoriesBaseVisitorParsedConfig 22 | > extends BaseVisitor { 23 | constructor(config: RawConfig, parsedConfig: ParsedConfig) { 24 | super(config, { 25 | ...parsedConfig, 26 | factoryName: getConfigValue(config.factoryName, "create{Type}Mock"), 27 | }); 28 | } 29 | 30 | protected convertFactoryName( 31 | ...args: Parameters 32 | ): string { 33 | return this.config.factoryName.replace("{Type}", this.convertName(...args)); 34 | } 35 | 36 | protected convertNameWithNamespace( 37 | name: string, 38 | namespace: string | undefined 39 | ) { 40 | const convertedName = this.convertName(name); 41 | return namespace ? `${namespace}.${convertedName}` : convertedName; 42 | } 43 | 44 | protected print(lines: PrintLines, count = 0): string { 45 | return lines 46 | .map((line) => { 47 | if (Array.isArray(line)) { 48 | return this.print(line, count + 1); 49 | } 50 | return indent(line, count); 51 | }) 52 | .join("\n"); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/graphql-codegen-factories/src/operations/FactoriesOperationsVisitor.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { getBaseType } from "@graphql-codegen/plugin-helpers"; 3 | import { getConfigValue } from "@graphql-codegen/visitor-plugin-common"; 4 | import { camelCase, pascalCase } from "change-case-all"; 5 | import { 6 | FragmentDefinitionNode, 7 | GraphQLCompositeType, 8 | GraphQLInterfaceType, 9 | GraphQLObjectType, 10 | GraphQLOutputType, 11 | GraphQLSchema, 12 | isInterfaceType, 13 | isListType, 14 | isNonNullType, 15 | isUnionType, 16 | Kind, 17 | OperationDefinitionNode, 18 | SelectionNode, 19 | } from "graphql"; 20 | import { 21 | FactoriesBaseVisitor, 22 | FactoriesBaseVisitorParsedConfig, 23 | FactoriesBaseVisitorRawConfig, 24 | } from "../FactoriesBaseVisitor"; 25 | 26 | export interface FactoriesOperationsVisitorRawConfig 27 | extends FactoriesBaseVisitorRawConfig { 28 | schemaFactoriesPath?: string; 29 | namespacedSchemaFactoriesImportName?: string; 30 | } 31 | 32 | export interface FactoriesOperationsVisitorParsedConfig 33 | extends FactoriesBaseVisitorParsedConfig { 34 | schemaFactoriesPath: string; 35 | namespacedSchemaFactoriesImportName: string; 36 | } 37 | 38 | interface NormalizedSelection { 39 | name: string; 40 | alias: string | undefined; 41 | typeCondition: GraphQLCompositeType; 42 | type: GraphQLOutputType; 43 | selections: Record | undefined; 44 | } 45 | interface SelectionAncestor { 46 | selection: NormalizedSelection; 47 | typeCondition: string | undefined; 48 | } 49 | 50 | export class FactoriesOperationsVisitor extends FactoriesBaseVisitor< 51 | FactoriesOperationsVisitorRawConfig, 52 | FactoriesOperationsVisitorParsedConfig 53 | > { 54 | private schema: GraphQLSchema; 55 | private fragments: FragmentDefinitionNode[]; 56 | private unnamedCounter = 1; 57 | 58 | constructor( 59 | schema: GraphQLSchema, 60 | fragments: FragmentDefinitionNode[], 61 | config: FactoriesOperationsVisitorRawConfig, 62 | outputFile: string | undefined 63 | ) { 64 | const parsedConfig = { 65 | schemaFactoriesPath: getConfigValue( 66 | config.schemaFactoriesPath, 67 | undefined 68 | ), 69 | namespacedSchemaFactoriesImportName: getConfigValue( 70 | config.namespacedSchemaFactoriesImportName, 71 | undefined 72 | ), 73 | } as FactoriesOperationsVisitorParsedConfig; 74 | 75 | if (parsedConfig.schemaFactoriesPath && outputFile) { 76 | const outputDirectory = path.dirname(outputFile); 77 | const schemaFactoriesPath = path.resolve( 78 | process.cwd(), 79 | parsedConfig.schemaFactoriesPath 80 | ); 81 | const relativeFactoriesPath = path.relative( 82 | outputDirectory, 83 | schemaFactoriesPath 84 | ); 85 | 86 | // If the factories are located in the same directory as the file, 87 | // the path will look like "generated/factories.ts" instead of "./generated/factories.ts". 88 | // So we need to add the ./ at the beginning in this case. 89 | parsedConfig.schemaFactoriesPath = relativeFactoriesPath.startsWith(".") 90 | ? relativeFactoriesPath 91 | : `./${relativeFactoriesPath}`; 92 | } 93 | 94 | if ( 95 | parsedConfig.schemaFactoriesPath && 96 | parsedConfig.namespacedSchemaFactoriesImportName == null 97 | ) { 98 | parsedConfig.namespacedSchemaFactoriesImportName = "schemaFactories"; 99 | } 100 | 101 | super(config, parsedConfig); 102 | 103 | this.schema = schema; 104 | this.fragments = fragments; 105 | } 106 | 107 | private convertNameWithFactoriesNamespace(name: string) { 108 | return this.config.namespacedSchemaFactoriesImportName 109 | ? `${this.config.namespacedSchemaFactoriesImportName}.${name}` 110 | : name; 111 | } 112 | 113 | private handleAnonymousOperation( 114 | node: Pick 115 | ): string { 116 | const name = node.name && node.name.value; 117 | 118 | if (name) { 119 | return this.convertName(name, { 120 | useTypesPrefix: false, 121 | useTypesSuffix: false, 122 | }); 123 | } 124 | 125 | return this.convertName(String(this.unnamedCounter++), { 126 | prefix: "Unnamed_", 127 | suffix: "_", 128 | useTypesPrefix: false, 129 | useTypesSuffix: false, 130 | }); 131 | } 132 | 133 | public getImports() { 134 | const imports: string[] = []; 135 | 136 | if (this.config.schemaFactoriesPath) { 137 | imports.push( 138 | `import * as ${ 139 | this.config.namespacedSchemaFactoriesImportName 140 | } from "${this.config.schemaFactoriesPath.replace( 141 | /\.(js|ts|d.ts)$/, 142 | "" 143 | )}";` 144 | ); 145 | } 146 | 147 | return imports; 148 | } 149 | 150 | private groupSelections( 151 | selections: NormalizedSelection[] 152 | ): Record { 153 | return selections 154 | .filter( 155 | (selection, index, selections) => 156 | selections.findIndex( 157 | (otherSelection) => 158 | otherSelection.alias === selection.alias && 159 | otherSelection.name === selection.name && 160 | otherSelection.typeCondition.name === selection.typeCondition.name 161 | ) === index 162 | ) 163 | .reduce>((acc, selection) => { 164 | acc[selection.typeCondition.name] = ( 165 | acc[selection.typeCondition.name] ?? [] 166 | ).concat(selection); 167 | return acc; 168 | }, {}); 169 | } 170 | 171 | private normalizeSelection( 172 | parent: GraphQLCompositeType, 173 | selection: OperationDefinitionNode | SelectionNode 174 | ): NormalizedSelection[] { 175 | if (selection.kind === Kind.OPERATION_DEFINITION) { 176 | const operationSuffix = this.getOperationSuffix( 177 | selection, 178 | pascalCase(selection.operation) 179 | ); 180 | const name = this.convertName(this.handleAnonymousOperation(selection), { 181 | suffix: operationSuffix, 182 | }); 183 | 184 | return [ 185 | { 186 | name: name, 187 | alias: this.convertFactoryName(name), 188 | typeCondition: parent, 189 | type: parent, 190 | selections: this.groupSelections( 191 | selection.selectionSet.selections.flatMap((selection) => 192 | this.normalizeSelection(parent, selection) 193 | ) 194 | ), 195 | }, 196 | ]; 197 | } 198 | 199 | if (selection.kind === Kind.FIELD) { 200 | const type = (parent as GraphQLObjectType).getFields()[ 201 | selection.name.value 202 | ].type; 203 | return [ 204 | { 205 | name: selection.name.value, 206 | alias: selection.alias?.value, 207 | typeCondition: parent, 208 | type: type, 209 | selections: 210 | selection.selectionSet == null 211 | ? undefined 212 | : this.groupSelections( 213 | selection.selectionSet.selections.flatMap((childSelection) => 214 | this.normalizeSelection( 215 | getBaseType(type) as GraphQLCompositeType, 216 | childSelection 217 | ) 218 | ) 219 | ), 220 | }, 221 | ]; 222 | } 223 | 224 | let typeCondition = parent; 225 | let selections: readonly SelectionNode[] = []; 226 | 227 | if (selection.kind === Kind.FRAGMENT_SPREAD) { 228 | const fragment = this.fragments.find( 229 | (otherFragment) => otherFragment.name.value === selection.name.value 230 | ); 231 | 232 | if (fragment == null) { 233 | throw new Error(`Fragment "${selection.name.value}" not found`); 234 | } 235 | 236 | const newTypeCondition = this.schema.getType( 237 | fragment.typeCondition.name.value 238 | ) as GraphQLObjectType | GraphQLInterfaceType | undefined; 239 | 240 | if (newTypeCondition == null) { 241 | throw new Error( 242 | `Fragment "${fragment.name.value}"'s type condition "${fragment.typeCondition.name.value}" not found` 243 | ); 244 | } 245 | 246 | typeCondition = newTypeCondition; 247 | selections = fragment.selectionSet.selections; 248 | } 249 | 250 | if (selection.kind === Kind.INLINE_FRAGMENT) { 251 | if (selection.typeCondition) { 252 | const newTypeCondition = this.schema.getType( 253 | selection.typeCondition.name.value 254 | ) as GraphQLObjectType | GraphQLInterfaceType | undefined; 255 | 256 | if (newTypeCondition == null) { 257 | throw new Error( 258 | `Inline fragment's type condition "${selection.typeCondition.name.value}" not found` 259 | ); 260 | } 261 | 262 | typeCondition = newTypeCondition; 263 | } 264 | 265 | selections = selection.selectionSet.selections; 266 | } 267 | 268 | let typeConditions = [typeCondition]; 269 | 270 | if (isInterfaceType(typeCondition)) { 271 | typeConditions = isUnionType(parent) 272 | ? parent 273 | .getTypes() 274 | .filter((type) => 275 | type 276 | .getInterfaces() 277 | .some((inter) => inter.name === typeCondition.name) 278 | ) 279 | : [parent]; 280 | } 281 | 282 | return typeConditions.flatMap((otherTypeCondition) => 283 | selections.flatMap((childSelection) => 284 | this.normalizeSelection(otherTypeCondition, childSelection) 285 | ) 286 | ); 287 | } 288 | 289 | private convertOperationFactoryName(ancestors: SelectionAncestor[]): string { 290 | return ancestors 291 | .flatMap(({ selection, typeCondition }) => [ 292 | selection.alias ?? selection.name, 293 | ...(isUnionType(getBaseType(selection.type)) && typeCondition 294 | ? [typeCondition] 295 | : []), 296 | ]) 297 | .join("_"); 298 | } 299 | 300 | private wrapWithModifiers( 301 | returnType: string, 302 | type: GraphQLOutputType, 303 | isNullable = true 304 | ): string { 305 | if (isNonNullType(type)) { 306 | return this.wrapWithModifiers(returnType, type.ofType, false); 307 | } 308 | 309 | const updatedReturnType = isNullable 310 | ? `NonNullable<${returnType}>` 311 | : returnType; 312 | 313 | if (isListType(type)) { 314 | return this.wrapWithModifiers( 315 | `${updatedReturnType}[number]`, 316 | type.ofType 317 | ); 318 | } 319 | 320 | return updatedReturnType; 321 | } 322 | 323 | private getReturnType([ 324 | { selection: operation }, 325 | ...selections 326 | ]: SelectionAncestor[]): string { 327 | return selections.reduce((acc, { selection, typeCondition }) => { 328 | const withModifiers = this.wrapWithModifiers( 329 | `${acc}["${selection.name}"]`, 330 | selection.type 331 | ); 332 | return isUnionType(getBaseType(selection.type)) && typeCondition 333 | ? `Extract<${withModifiers} & { __typename: "${typeCondition}" }, { __typename: "${typeCondition}" }>` 334 | : withModifiers; 335 | }, operation.name); 336 | } 337 | 338 | private generateFactories( 339 | selection: NormalizedSelection, 340 | ancestors: SelectionAncestor[] = [] 341 | ): string[] { 342 | if (selection.selections == null) { 343 | return []; 344 | } 345 | 346 | const factories: string[] = []; 347 | 348 | if (isUnionType(getBaseType(selection.type))) { 349 | const futureAncestors = ancestors.concat({ 350 | selection, 351 | typeCondition: undefined, 352 | }); 353 | const factoryName = this.convertOperationFactoryName(futureAncestors); 354 | const returnType = this.getReturnType(futureAncestors); 355 | const typeConditions = Object.keys(selection.selections); 356 | const defaultTypeCondition = typeConditions[0]; 357 | 358 | factories.push( 359 | this.print([ 360 | `export function ${factoryName}(props: Partial<${returnType}> = {}): ${returnType} {`, 361 | [ 362 | `switch(props.__typename) {`, 363 | ...typeConditions.map((typeCondition) => [ 364 | `case "${typeCondition}":`, 365 | [ 366 | `return ${this.convertOperationFactoryName( 367 | ancestors.concat({ selection, typeCondition }) 368 | )}(props);`, 369 | ], 370 | ]), 371 | [ 372 | `case undefined:`, 373 | `default:`, 374 | [ 375 | `return ${factoryName}({ ...props, __typename: "${defaultTypeCondition}" })`, 376 | ], 377 | ], 378 | `}`, 379 | ], 380 | `}`, 381 | ]) 382 | ); 383 | } 384 | 385 | Object.entries(selection.selections).forEach( 386 | ([typeCondition, childSelections]) => { 387 | const futureAncestors = ancestors.concat({ selection, typeCondition }); 388 | const factoryName = this.convertOperationFactoryName(futureAncestors); 389 | const returnType = this.getReturnType(futureAncestors); 390 | const objectVarName = camelCase(typeCondition); 391 | const scalars = childSelections.filter( 392 | (childSelection) => childSelection.selections == null 393 | ); 394 | 395 | factories.push( 396 | this.print([ 397 | `export function ${factoryName}(props: Partial<${returnType}> = {}): ${returnType} {`, 398 | [ 399 | ...(scalars.length > 0 400 | ? [ 401 | `const ${objectVarName} = ${this.convertNameWithFactoriesNamespace( 402 | this.convertFactoryName(typeCondition) 403 | )}({`, 404 | scalars.map( 405 | (scalar) => 406 | `${scalar.name}: props.${scalar.alias ?? scalar.name},` 407 | ), 408 | `});`, 409 | ] 410 | : []), 411 | `return {`, 412 | [ 413 | `__typename: "${typeCondition}",`, 414 | ...childSelections.map((childSelection) => { 415 | let value = `${objectVarName}.${childSelection.name}`; 416 | 417 | if (childSelection.selections) { 418 | if (isNonNullType(childSelection.type)) { 419 | value = isListType(childSelection.type.ofType) 420 | ? "[]" 421 | : `${this.convertOperationFactoryName( 422 | futureAncestors.concat({ 423 | selection: childSelection, 424 | typeCondition, 425 | }) 426 | )}({})`; 427 | } else { 428 | value = "null"; 429 | } 430 | } 431 | 432 | return `${ 433 | childSelection.alias ?? childSelection.name 434 | }: ${value},`; 435 | }), 436 | `...props,`, 437 | ], 438 | `};`, 439 | ], 440 | `}`, 441 | ]) 442 | ); 443 | 444 | childSelections.forEach((childSelection) => { 445 | if (childSelection.selections) { 446 | factories.push( 447 | ...this.generateFactories(childSelection, futureAncestors) 448 | ); 449 | } 450 | }); 451 | } 452 | ); 453 | 454 | return factories; 455 | } 456 | 457 | OperationDefinition(node: OperationDefinitionNode): string { 458 | const rootType = this.schema.getRootType(node.operation); 459 | 460 | if (rootType == null) { 461 | throw new Error(`Root type "${node.operation}" not found`); 462 | } 463 | 464 | return this.normalizeSelection(rootType, node) 465 | .flatMap((selection) => this.generateFactories(selection)) 466 | .join("\n\n"); 467 | } 468 | } 469 | -------------------------------------------------------------------------------- /packages/graphql-codegen-factories/src/operations/__tests__/__snapshots__/plugin.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`plugin should add interface's selections to the matching types 1`] = ` 4 | Object { 5 | "content": "export function createGetMediasQueryMock(props: Partial = {}): GetMediasQuery { 6 | return { 7 | __typename: \\"Query\\", 8 | medias: [], 9 | ...props, 10 | }; 11 | } 12 | 13 | export function createGetMediasQueryMock_medias(props: Partial = {}): GetMediasQuery[\\"medias\\"][number] { 14 | switch(props.__typename) { 15 | case \\"Image\\": 16 | return createGetMediasQueryMock_medias_Image(props); 17 | case \\"Audio\\": 18 | return createGetMediasQueryMock_medias_Audio(props); 19 | case \\"Video\\": 20 | return createGetMediasQueryMock_medias_Video(props); 21 | case undefined: 22 | default: 23 | return createGetMediasQueryMock_medias({ ...props, __typename: \\"Image\\" }) 24 | } 25 | } 26 | 27 | export function createGetMediasQueryMock_medias_Image(props: Partial> = {}): Extract { 28 | const image = schemaFactories.createImageMock({ 29 | path: props.path, 30 | width: props.width, 31 | }); 32 | return { 33 | __typename: \\"Image\\", 34 | path: image.path, 35 | width: image.width, 36 | ...props, 37 | }; 38 | } 39 | 40 | export function createGetMediasQueryMock_medias_Audio(props: Partial> = {}): Extract { 41 | const audio = schemaFactories.createAudioMock({ 42 | path: props.path, 43 | length: props.length, 44 | }); 45 | return { 46 | __typename: \\"Audio\\", 47 | path: audio.path, 48 | length: audio.length, 49 | ...props, 50 | }; 51 | } 52 | 53 | export function createGetMediasQueryMock_medias_Video(props: Partial> = {}): Extract { 54 | const video = schemaFactories.createVideoMock({ 55 | url: props.url, 56 | length: props.length, 57 | }); 58 | return { 59 | __typename: \\"Video\\", 60 | url: video.url, 61 | length: video.length, 62 | ...props, 63 | }; 64 | }", 65 | "prepend": Array [ 66 | "import * as schemaFactories from \\"./factories\\";", 67 | ], 68 | } 69 | `; 70 | 71 | exports[`plugin should dedupe fields 1`] = ` 72 | Object { 73 | "content": "export function createGetMeQueryMock(props: Partial = {}): GetMeQuery { 74 | return { 75 | __typename: \\"Query\\", 76 | me: null, 77 | ...props, 78 | }; 79 | } 80 | 81 | export function createGetMeQueryMock_me(props: Partial> = {}): NonNullable { 82 | switch(props.__typename) { 83 | case \\"User\\": 84 | return createGetMeQueryMock_me_User(props); 85 | case \\"Admin\\": 86 | return createGetMeQueryMock_me_Admin(props); 87 | case undefined: 88 | default: 89 | return createGetMeQueryMock_me({ ...props, __typename: \\"User\\" }) 90 | } 91 | } 92 | 93 | export function createGetMeQueryMock_me_User(props: Partial & { __typename: \\"User\\" }, { __typename: \\"User\\" }>> = {}): Extract & { __typename: \\"User\\" }, { __typename: \\"User\\" }> { 94 | const user = schemaFactories.createUserMock({ 95 | id: props.id, 96 | username: props.username, 97 | id: props.userId, 98 | }); 99 | return { 100 | __typename: \\"User\\", 101 | id: user.id, 102 | username: user.username, 103 | userId: user.id, 104 | ...props, 105 | }; 106 | } 107 | 108 | export function createGetMeQueryMock_me_Admin(props: Partial & { __typename: \\"Admin\\" }, { __typename: \\"Admin\\" }>> = {}): Extract & { __typename: \\"Admin\\" }, { __typename: \\"Admin\\" }> { 109 | const admin = schemaFactories.createAdminMock({ 110 | id: props.id, 111 | }); 112 | return { 113 | __typename: \\"Admin\\", 114 | id: admin.id, 115 | ...props, 116 | }; 117 | }", 118 | "prepend": Array [ 119 | "import * as schemaFactories from \\"./factories\\";", 120 | ], 121 | } 122 | `; 123 | 124 | exports[`plugin should generate factory for a simple operation 1`] = ` 125 | Object { 126 | "content": "export function createCreateUserMutationMock(props: Partial = {}): CreateUserMutation { 127 | return { 128 | __typename: \\"Mutation\\", 129 | createUser: createCreateUserMutationMock_createUser({}), 130 | ...props, 131 | }; 132 | } 133 | 134 | export function createCreateUserMutationMock_createUser(props: Partial = {}): CreateUserMutation[\\"createUser\\"] { 135 | const user = schemaFactories.createUserMock({ 136 | id: props.id, 137 | username: props.username, 138 | }); 139 | return { 140 | __typename: \\"User\\", 141 | id: user.id, 142 | username: user.username, 143 | ...props, 144 | }; 145 | }", 146 | "prepend": Array [ 147 | "import * as schemaFactories from \\"./factories\\";", 148 | ], 149 | } 150 | `; 151 | 152 | exports[`plugin should generate union factory even when querying one type from the union 1`] = ` 153 | Object { 154 | "content": "export function createGetMediasQueryMock(props: Partial = {}): GetMediasQuery { 155 | return { 156 | __typename: \\"Query\\", 157 | medias: [], 158 | ...props, 159 | }; 160 | } 161 | 162 | export function createGetMediasQueryMock_medias(props: Partial = {}): GetMediasQuery[\\"medias\\"][number] { 163 | switch(props.__typename) { 164 | case \\"Audio\\": 165 | return createGetMediasQueryMock_medias_Audio(props); 166 | case undefined: 167 | default: 168 | return createGetMediasQueryMock_medias({ ...props, __typename: \\"Audio\\" }) 169 | } 170 | } 171 | 172 | export function createGetMediasQueryMock_medias_Audio(props: Partial> = {}): Extract { 173 | const audio = schemaFactories.createAudioMock({ 174 | length: props.length, 175 | }); 176 | return { 177 | __typename: \\"Audio\\", 178 | length: audio.length, 179 | ...props, 180 | }; 181 | }", 182 | "prepend": Array [ 183 | "import * as schemaFactories from \\"./factories\\";", 184 | ], 185 | } 186 | `; 187 | 188 | exports[`plugin should merge fragments and inline fragments with the same type condition 1`] = ` 189 | Object { 190 | "content": "export function createGetMeQueryMock(props: Partial = {}): GetMeQuery { 191 | return { 192 | __typename: \\"Query\\", 193 | me: createGetMeQueryMock_me({}), 194 | ...props, 195 | }; 196 | } 197 | 198 | export function createGetMeQueryMock_me(props: Partial = {}): GetMeQuery[\\"me\\"] { 199 | const user = schemaFactories.createUserMock({ 200 | id: props.id, 201 | username: props.username, 202 | }); 203 | return { 204 | __typename: \\"User\\", 205 | id: user.id, 206 | username: user.username, 207 | ...props, 208 | }; 209 | }", 210 | "prepend": Array [ 211 | "import * as schemaFactories from \\"./factories\\";", 212 | ], 213 | } 214 | `; 215 | 216 | exports[`plugin should merge fragments with the same type condition 1`] = ` 217 | Object { 218 | "content": "export function createGetMeQueryMock(props: Partial = {}): GetMeQuery { 219 | return { 220 | __typename: \\"Query\\", 221 | me: createGetMeQueryMock_me({}), 222 | ...props, 223 | }; 224 | } 225 | 226 | export function createGetMeQueryMock_me(props: Partial = {}): GetMeQuery[\\"me\\"] { 227 | const user = schemaFactories.createUserMock({ 228 | id: props.id, 229 | username: props.username, 230 | }); 231 | return { 232 | __typename: \\"User\\", 233 | id: user.id, 234 | username: user.username, 235 | ...props, 236 | }; 237 | }", 238 | "prepend": Array [ 239 | "import * as schemaFactories from \\"./factories\\";", 240 | ], 241 | } 242 | `; 243 | 244 | exports[`plugin should merge inline fragments with the same type condition 1`] = ` 245 | Object { 246 | "content": "export function createGetMeQueryMock(props: Partial = {}): GetMeQuery { 247 | return { 248 | __typename: \\"Query\\", 249 | me: createGetMeQueryMock_me({}), 250 | ...props, 251 | }; 252 | } 253 | 254 | export function createGetMeQueryMock_me(props: Partial = {}): GetMeQuery[\\"me\\"] { 255 | const user = schemaFactories.createUserMock({ 256 | id: props.id, 257 | username: props.username, 258 | }); 259 | return { 260 | __typename: \\"User\\", 261 | id: user.id, 262 | username: user.username, 263 | ...props, 264 | }; 265 | }", 266 | "prepend": Array [ 267 | "import * as schemaFactories from \\"./factories\\";", 268 | ], 269 | } 270 | `; 271 | 272 | exports[`plugin should support aliases 1`] = ` 273 | Object { 274 | "content": "export function createCreateUserMutationMock(props: Partial = {}): CreateUserMutation { 275 | return { 276 | __typename: \\"Mutation\\", 277 | createUser: createCreateUserMutationMock_createUser({}), 278 | ...props, 279 | }; 280 | } 281 | 282 | export function createCreateUserMutationMock_createUser(props: Partial = {}): CreateUserMutation[\\"createUser\\"] { 283 | const user = schemaFactories.createUserMock({ 284 | id: props.id, 285 | username: props.email, 286 | }); 287 | return { 288 | __typename: \\"User\\", 289 | id: user.id, 290 | email: user.username, 291 | ...props, 292 | }; 293 | }", 294 | "prepend": Array [ 295 | "import * as schemaFactories from \\"./factories\\";", 296 | ], 297 | } 298 | `; 299 | 300 | exports[`plugin should support custom root Query 1`] = ` 301 | Object { 302 | "content": "export function createGetUserQueryMock(props: Partial = {}): GetUserQuery { 303 | return { 304 | __typename: \\"CustomQuery\\", 305 | user: null, 306 | ...props, 307 | }; 308 | } 309 | 310 | export function createGetUserQueryMock_user(props: Partial> = {}): NonNullable { 311 | const user = schemaFactories.createUserMock({ 312 | username: props.username, 313 | }); 314 | return { 315 | __typename: \\"User\\", 316 | username: user.username, 317 | ...props, 318 | }; 319 | }", 320 | "prepend": Array [ 321 | "import * as schemaFactories from \\"./factories\\";", 322 | ], 323 | } 324 | `; 325 | 326 | exports[`plugin should support external fragments 1`] = ` 327 | Object { 328 | "content": "export function createGetMeQueryMock(props: Partial = {}): GetMeQuery { 329 | return { 330 | __typename: \\"Query\\", 331 | me: createGetMeQueryMock_me({}), 332 | ...props, 333 | }; 334 | } 335 | 336 | export function createGetMeQueryMock_me(props: Partial = {}): GetMeQuery[\\"me\\"] { 337 | const user = schemaFactories.createUserMock({ 338 | id: props.id, 339 | username: props.username, 340 | }); 341 | return { 342 | __typename: \\"User\\", 343 | id: user.id, 344 | username: user.username, 345 | ...props, 346 | }; 347 | }", 348 | "prepend": Array [ 349 | "import * as schemaFactories from \\"./factories\\";", 350 | ], 351 | } 352 | `; 353 | 354 | exports[`plugin should support fragments 1`] = ` 355 | Object { 356 | "content": "export function createGetMeQueryMock(props: Partial = {}): GetMeQuery { 357 | return { 358 | __typename: \\"Query\\", 359 | me: createGetMeQueryMock_me({}), 360 | ...props, 361 | }; 362 | } 363 | 364 | export function createGetMeQueryMock_me(props: Partial = {}): GetMeQuery[\\"me\\"] { 365 | const user = schemaFactories.createUserMock({ 366 | id: props.id, 367 | username: props.username, 368 | }); 369 | return { 370 | __typename: \\"User\\", 371 | id: user.id, 372 | username: user.username, 373 | ...props, 374 | }; 375 | }", 376 | "prepend": Array [ 377 | "import * as schemaFactories from \\"./factories\\";", 378 | ], 379 | } 380 | `; 381 | 382 | exports[`plugin should support inline fragments 1`] = ` 383 | Object { 384 | "content": "export function createGetMeQueryMock(props: Partial = {}): GetMeQuery { 385 | return { 386 | __typename: \\"Query\\", 387 | me: createGetMeQueryMock_me({}), 388 | ...props, 389 | }; 390 | } 391 | 392 | export function createGetMeQueryMock_me(props: Partial = {}): GetMeQuery[\\"me\\"] { 393 | const user = schemaFactories.createUserMock({ 394 | id: props.id, 395 | username: props.username, 396 | }); 397 | return { 398 | __typename: \\"User\\", 399 | id: user.id, 400 | username: user.username, 401 | ...props, 402 | }; 403 | }", 404 | "prepend": Array [ 405 | "import * as schemaFactories from \\"./factories\\";", 406 | ], 407 | } 408 | `; 409 | 410 | exports[`plugin should support lists 1`] = ` 411 | Object { 412 | "content": "export function createGetUsersQueryMock(props: Partial = {}): GetUsersQuery { 413 | return { 414 | __typename: \\"Query\\", 415 | users: [], 416 | ...props, 417 | }; 418 | } 419 | 420 | export function createGetUsersQueryMock_users(props: Partial = {}): GetUsersQuery[\\"users\\"][number] { 421 | const user = schemaFactories.createUserMock({ 422 | id: props.id, 423 | username: props.username, 424 | }); 425 | return { 426 | __typename: \\"User\\", 427 | id: user.id, 428 | username: user.username, 429 | ...props, 430 | }; 431 | }", 432 | "prepend": Array [ 433 | "import * as schemaFactories from \\"./factories\\";", 434 | ], 435 | } 436 | `; 437 | 438 | exports[`plugin should support nested selections 1`] = ` 439 | Object { 440 | "content": "export function createGetMeQueryMock(props: Partial = {}): GetMeQuery { 441 | return { 442 | __typename: \\"Query\\", 443 | me: null, 444 | ...props, 445 | }; 446 | } 447 | 448 | export function createGetMeQueryMock_me(props: Partial> = {}): NonNullable { 449 | const user = schemaFactories.createUserMock({ 450 | id: props.id, 451 | username: props.username, 452 | }); 453 | return { 454 | __typename: \\"User\\", 455 | id: user.id, 456 | username: user.username, 457 | followers: null, 458 | ...props, 459 | }; 460 | } 461 | 462 | export function createGetMeQueryMock_me_followers(props: Partial[\\"followers\\"]>[number]>> = {}): NonNullable[\\"followers\\"]>[number]> { 463 | const user = schemaFactories.createUserMock({ 464 | id: props.id, 465 | }); 466 | return { 467 | __typename: \\"User\\", 468 | id: user.id, 469 | ...props, 470 | }; 471 | }", 472 | "prepend": Array [ 473 | "import * as schemaFactories from \\"./factories\\";", 474 | ], 475 | } 476 | `; 477 | 478 | exports[`plugin should support unions 1`] = ` 479 | Object { 480 | "content": "export function createGetMediasQueryMock(props: Partial = {}): GetMediasQuery { 481 | return { 482 | __typename: \\"Query\\", 483 | medias: [], 484 | ...props, 485 | }; 486 | } 487 | 488 | export function createGetMediasQueryMock_medias(props: Partial = {}): GetMediasQuery[\\"medias\\"][number] { 489 | switch(props.__typename) { 490 | case \\"Image\\": 491 | return createGetMediasQueryMock_medias_Image(props); 492 | case \\"Video\\": 493 | return createGetMediasQueryMock_medias_Video(props); 494 | case undefined: 495 | default: 496 | return createGetMediasQueryMock_medias({ ...props, __typename: \\"Image\\" }) 497 | } 498 | } 499 | 500 | export function createGetMediasQueryMock_medias_Image(props: Partial> = {}): Extract { 501 | const image = schemaFactories.createImageMock({ 502 | src: props.src, 503 | }); 504 | return { 505 | __typename: \\"Image\\", 506 | src: image.src, 507 | dimensions: null, 508 | ...props, 509 | }; 510 | } 511 | 512 | export function createGetMediasQueryMock_medias_Image_dimensions(props: Partial[\\"dimensions\\"]>> = {}): NonNullable[\\"dimensions\\"]> { 513 | const imageDimensions = schemaFactories.createImageDimensionsMock({ 514 | width: props.width, 515 | }); 516 | return { 517 | __typename: \\"ImageDimensions\\", 518 | width: imageDimensions.width, 519 | ...props, 520 | }; 521 | } 522 | 523 | export function createGetMediasQueryMock_medias_Video(props: Partial> = {}): Extract { 524 | const video = schemaFactories.createVideoMock({ 525 | href: props.href, 526 | }); 527 | return { 528 | __typename: \\"Video\\", 529 | href: video.href, 530 | dimensions: null, 531 | ...props, 532 | }; 533 | } 534 | 535 | export function createGetMediasQueryMock_medias_Video_dimensions(props: Partial[\\"dimensions\\"]>> = {}): NonNullable[\\"dimensions\\"]> { 536 | const imageDimensions = schemaFactories.createImageDimensionsMock({ 537 | height: props.height, 538 | }); 539 | return { 540 | __typename: \\"ImageDimensions\\", 541 | height: imageDimensions.height, 542 | ...props, 543 | }; 544 | }", 545 | "prepend": Array [ 546 | "import * as schemaFactories from \\"./factories\\";", 547 | ], 548 | } 549 | `; 550 | 551 | exports[`plugin should support unnamed operations 1`] = ` 552 | Object { 553 | "content": "export function createUnnamed_1_QueryMock(props: Partial = {}): Unnamed_1_Query { 554 | return { 555 | __typename: \\"Query\\", 556 | me: null, 557 | ...props, 558 | }; 559 | } 560 | 561 | export function createUnnamed_1_QueryMock_me(props: Partial> = {}): NonNullable { 562 | const user = schemaFactories.createUserMock({ 563 | id: props.id, 564 | username: props.username, 565 | }); 566 | return { 567 | __typename: \\"User\\", 568 | id: user.id, 569 | username: user.username, 570 | ...props, 571 | }; 572 | }", 573 | "prepend": Array [ 574 | "import * as schemaFactories from \\"./factories\\";", 575 | ], 576 | } 577 | `; 578 | 579 | exports[`plugin should unwrap inline fragments without a type condition 1`] = ` 580 | Object { 581 | "content": "export function createGetMeQueryMock(props: Partial = {}): GetMeQuery { 582 | return { 583 | __typename: \\"Query\\", 584 | me: createGetMeQueryMock_me({}), 585 | ...props, 586 | }; 587 | } 588 | 589 | export function createGetMeQueryMock_me(props: Partial = {}): GetMeQuery[\\"me\\"] { 590 | const user = schemaFactories.createUserMock({ 591 | id: props.id, 592 | username: props.username, 593 | }); 594 | return { 595 | __typename: \\"User\\", 596 | id: user.id, 597 | username: user.username, 598 | ...props, 599 | }; 600 | }", 601 | "prepend": Array [ 602 | "import * as schemaFactories from \\"./factories\\";", 603 | ], 604 | } 605 | `; 606 | -------------------------------------------------------------------------------- /packages/graphql-codegen-factories/src/operations/__tests__/plugin.ts: -------------------------------------------------------------------------------- 1 | import { buildSchema, parse, FragmentDefinitionNode, Kind } from "graphql"; 2 | import { plugin } from "../plugin"; 3 | 4 | describe("plugin", () => { 5 | it("should generate factory for a simple operation", async () => { 6 | const schema = buildSchema(/* GraphQL */ ` 7 | type User { 8 | id: ID! 9 | username: String! 10 | } 11 | 12 | type Mutation { 13 | createUser(username: String): User! 14 | } 15 | `); 16 | const ast = parse(/* GraphQL */ ` 17 | mutation CreateUser($username: String) { 18 | createUser(username: $username) { 19 | id 20 | username 21 | } 22 | } 23 | `); 24 | 25 | const output = await plugin( 26 | schema, 27 | [{ location: "CreateUser.graphql", document: ast }], 28 | { schemaFactoriesPath: "./factories" } 29 | ); 30 | expect(output).toMatchSnapshot(); 31 | }); 32 | 33 | it("should support aliases", async () => { 34 | const schema = buildSchema(/* GraphQL */ ` 35 | type User { 36 | id: ID! 37 | username: String! 38 | } 39 | 40 | type Mutation { 41 | createUser(username: String): User! 42 | } 43 | `); 44 | const ast = parse(/* GraphQL */ ` 45 | mutation CreateUser($username: String) { 46 | createUser(username: $username) { 47 | id 48 | email: username 49 | } 50 | } 51 | `); 52 | 53 | const output = await plugin( 54 | schema, 55 | [{ location: "CreateUser.graphql", document: ast }], 56 | { schemaFactoriesPath: "./factories" } 57 | ); 58 | expect(output).toMatchSnapshot(); 59 | }); 60 | 61 | it("should support fragments", async () => { 62 | const schema = buildSchema(/* GraphQL */ ` 63 | type User { 64 | id: ID! 65 | username: String! 66 | } 67 | 68 | type Query { 69 | me: User! 70 | } 71 | `); 72 | const ast = parse(/* GraphQL */ ` 73 | query GetMe { 74 | me { 75 | ...UserFragment 76 | } 77 | } 78 | fragment UserFragment on User { 79 | id 80 | username 81 | } 82 | `); 83 | 84 | const output = await plugin( 85 | schema, 86 | [{ location: "GetMe.graphql", document: ast }], 87 | { schemaFactoriesPath: "./factories" } 88 | ); 89 | expect(output).toMatchSnapshot(); 90 | }); 91 | 92 | it("should merge fragments with the same type condition", async () => { 93 | const schema = buildSchema(/* GraphQL */ ` 94 | type User { 95 | id: ID! 96 | username: String! 97 | } 98 | 99 | type Query { 100 | me: User! 101 | } 102 | `); 103 | const ast = parse(/* GraphQL */ ` 104 | query GetMe { 105 | me { 106 | ...UserIDFragment 107 | ...UserUsernameFragment 108 | } 109 | } 110 | fragment UserIDFragment on User { 111 | id 112 | } 113 | fragment UserUsernameFragment on User { 114 | username 115 | } 116 | `); 117 | 118 | const output = await plugin( 119 | schema, 120 | [{ location: "GetMe.graphql", document: ast }], 121 | { schemaFactoriesPath: "./factories" } 122 | ); 123 | expect(output).toMatchSnapshot(); 124 | }); 125 | 126 | it("should support inline fragments", async () => { 127 | const schema = buildSchema(/* GraphQL */ ` 128 | type User { 129 | id: ID! 130 | username: String! 131 | } 132 | 133 | type Query { 134 | me: User! 135 | } 136 | `); 137 | const ast = parse(/* GraphQL */ ` 138 | query GetMe { 139 | me { 140 | ... on User { 141 | id 142 | username 143 | } 144 | } 145 | } 146 | `); 147 | 148 | const output = await plugin( 149 | schema, 150 | [{ location: "GetMe.graphql", document: ast }], 151 | { schemaFactoriesPath: "./factories" } 152 | ); 153 | expect(output).toMatchSnapshot(); 154 | }); 155 | 156 | it("should merge inline fragments with the same type condition", async () => { 157 | const schema = buildSchema(/* GraphQL */ ` 158 | type User { 159 | id: ID! 160 | username: String! 161 | } 162 | 163 | type Query { 164 | me: User! 165 | } 166 | `); 167 | const ast = parse(/* GraphQL */ ` 168 | query GetMe { 169 | me { 170 | ... on User { 171 | id 172 | } 173 | ... on User { 174 | username 175 | } 176 | } 177 | } 178 | `); 179 | 180 | const output = await plugin( 181 | schema, 182 | [{ location: "GetMe.graphql", document: ast }], 183 | { schemaFactoriesPath: "./factories" } 184 | ); 185 | expect(output).toMatchSnapshot(); 186 | }); 187 | 188 | it("should unwrap inline fragments without a type condition", async () => { 189 | const schema = buildSchema(/* GraphQL */ ` 190 | type User { 191 | id: ID! 192 | username: String! 193 | } 194 | 195 | type Query { 196 | me: User! 197 | } 198 | `); 199 | const ast = parse(/* GraphQL */ ` 200 | query GetMe { 201 | me { 202 | ... { 203 | id 204 | ... { 205 | username 206 | } 207 | } 208 | } 209 | } 210 | `); 211 | 212 | const output = await plugin( 213 | schema, 214 | [{ location: "GetMe.graphql", document: ast }], 215 | { schemaFactoriesPath: "./factories" } 216 | ); 217 | expect(output).toMatchSnapshot(); 218 | }); 219 | 220 | it("should merge fragments and inline fragments with the same type condition", async () => { 221 | const schema = buildSchema(/* GraphQL */ ` 222 | type User { 223 | id: ID! 224 | username: String! 225 | } 226 | 227 | type Query { 228 | me: User! 229 | } 230 | `); 231 | const ast = parse(/* GraphQL */ ` 232 | query GetMe { 233 | me { 234 | ... on User { 235 | id 236 | } 237 | ...UserUsernameFragment 238 | } 239 | } 240 | fragment UserUsernameFragment on User { 241 | username 242 | } 243 | `); 244 | 245 | const output = await plugin( 246 | schema, 247 | [{ location: "GetMe.graphql", document: ast }], 248 | { schemaFactoriesPath: "./factories" } 249 | ); 250 | expect(output).toMatchSnapshot(); 251 | }); 252 | 253 | it("should support external fragments", async () => { 254 | const schema = buildSchema(/* GraphQL */ ` 255 | type User { 256 | id: ID! 257 | username: String! 258 | } 259 | 260 | type Query { 261 | me: User! 262 | } 263 | `); 264 | 265 | const fragments = parse(/* GraphQL */ ` 266 | fragment UserFragment on User { 267 | id 268 | username 269 | } 270 | `); 271 | 272 | const allFragments = fragments.definitions.filter( 273 | (d) => d.kind === Kind.FRAGMENT_DEFINITION 274 | ) as FragmentDefinitionNode[]; 275 | 276 | const externalFragments = allFragments.map((frag) => ({ 277 | isExternal: true, 278 | importFrom: frag.name.value, 279 | name: frag.name.value, 280 | onType: frag.typeCondition.name.value, 281 | node: frag, 282 | })); 283 | 284 | const ast = parse(/* GraphQL */ ` 285 | query GetMe { 286 | me { 287 | ...UserFragment 288 | } 289 | } 290 | `); 291 | 292 | const output = await plugin( 293 | schema, 294 | [{ location: "GetMe.graphql", document: ast }], 295 | { 296 | schemaFactoriesPath: "./factories", 297 | externalFragments, 298 | } 299 | ); 300 | expect(output).toMatchSnapshot(); 301 | }); 302 | 303 | it("should support unnamed operations", async () => { 304 | const schema = buildSchema(/* GraphQL */ ` 305 | type User { 306 | id: ID! 307 | username: String! 308 | } 309 | 310 | type Query { 311 | me: User 312 | } 313 | `); 314 | const ast = parse(/* GraphQL */ ` 315 | query { 316 | me { 317 | id 318 | username 319 | } 320 | } 321 | `); 322 | 323 | const output = await plugin( 324 | schema, 325 | [{ location: "GetMe.graphql", document: ast }], 326 | { schemaFactoriesPath: "./factories" } 327 | ); 328 | expect(output).toMatchSnapshot(); 329 | }); 330 | 331 | it("should support lists", async () => { 332 | const schema = buildSchema(/* GraphQL */ ` 333 | type User { 334 | id: ID! 335 | username: String! 336 | } 337 | 338 | type Query { 339 | users: [User!]! 340 | } 341 | `); 342 | const ast = parse(/* GraphQL */ ` 343 | query GetUsers { 344 | users { 345 | id 346 | username 347 | } 348 | } 349 | `); 350 | 351 | const output = await plugin( 352 | schema, 353 | [{ location: "GetUsers.graphql", document: ast }], 354 | { schemaFactoriesPath: "./factories" } 355 | ); 356 | expect(output).toMatchSnapshot(); 357 | }); 358 | 359 | it("should support nested selections", async () => { 360 | const schema = buildSchema(/* GraphQL */ ` 361 | type User { 362 | id: ID! 363 | username: String! 364 | followers: [User] 365 | } 366 | 367 | type Query { 368 | me: User 369 | } 370 | `); 371 | const ast = parse(/* GraphQL */ ` 372 | query GetMe { 373 | me { 374 | id 375 | username 376 | followers { 377 | id 378 | } 379 | } 380 | } 381 | `); 382 | 383 | const output = await plugin( 384 | schema, 385 | [{ location: "GetMe.graphql", document: ast }], 386 | { schemaFactoriesPath: "./factories" } 387 | ); 388 | expect(output).toMatchSnapshot(); 389 | }); 390 | 391 | it("should support unions", async () => { 392 | const schema = buildSchema(/* GraphQL */ ` 393 | type ImageDimensions { 394 | width: Int! 395 | height: Int! 396 | } 397 | 398 | type Image { 399 | src: String! 400 | dimensions: ImageDimensions 401 | } 402 | 403 | type Video { 404 | href: String! 405 | dimensions: ImageDimensions 406 | } 407 | 408 | union Media = Image | Video 409 | 410 | type Query { 411 | medias: [Media!]! 412 | } 413 | `); 414 | const ast = parse(/* GraphQL */ ` 415 | query GetMedias { 416 | medias { 417 | ... on Image { 418 | src 419 | dimensions { 420 | width 421 | } 422 | } 423 | ... on Video { 424 | href 425 | dimensions { 426 | height 427 | } 428 | } 429 | } 430 | } 431 | `); 432 | 433 | const output = await plugin( 434 | schema, 435 | [{ location: "GetMedias.graphql", document: ast }], 436 | { schemaFactoriesPath: "./factories" } 437 | ); 438 | expect(output).toMatchSnapshot(); 439 | }); 440 | 441 | it("should add interface's selections to the matching types", async () => { 442 | const schema = buildSchema(/* GraphQL */ ` 443 | interface File { 444 | path: String! 445 | } 446 | 447 | type Image implements File { 448 | path: String! 449 | width: Int! 450 | height: Int! 451 | } 452 | 453 | type Audio implements File { 454 | path: String! 455 | length: Int! 456 | } 457 | 458 | interface Streamable { 459 | url: String! 460 | } 461 | 462 | type Video implements Streamable { 463 | url: String! 464 | length: Int! 465 | } 466 | 467 | union Media = Image | Audio | Video 468 | 469 | type Query { 470 | medias: [Media!]! 471 | } 472 | `); 473 | const ast = parse(/* GraphQL */ ` 474 | query GetMedias { 475 | medias { 476 | ... on File { 477 | path 478 | } 479 | ... on Streamable { 480 | url 481 | } 482 | ... on Image { 483 | width 484 | } 485 | ... on Audio { 486 | length 487 | } 488 | ... on Video { 489 | length 490 | } 491 | } 492 | } 493 | `); 494 | 495 | const output = await plugin( 496 | schema, 497 | [{ location: "GetMedias.graphql", document: ast }], 498 | { schemaFactoriesPath: "./factories" } 499 | ); 500 | expect(output).toMatchSnapshot(); 501 | }); 502 | 503 | it("should dedupe fields", async () => { 504 | const schema = buildSchema(/* GraphQL */ ` 505 | interface Node { 506 | id: ID! 507 | } 508 | 509 | type User implements Node { 510 | id: ID! 511 | username: String! 512 | } 513 | 514 | type Admin implements Node { 515 | id: ID! 516 | canDeleteUser: Boolean! 517 | } 518 | 519 | union Me = User | Admin 520 | 521 | type Query { 522 | me: Me 523 | } 524 | `); 525 | const ast = parse(/* GraphQL */ ` 526 | query GetMe { 527 | me { 528 | ... on Node { 529 | id 530 | } 531 | ... on User { 532 | ...UserFragment 533 | id 534 | userId: id 535 | username 536 | } 537 | ... on Admin { 538 | id 539 | } 540 | } 541 | } 542 | fragment UserFragment on User { 543 | id 544 | username 545 | } 546 | `); 547 | 548 | const output = await plugin( 549 | schema, 550 | [{ location: "GetMe.graphql", document: ast }], 551 | { schemaFactoriesPath: "./factories" } 552 | ); 553 | expect(output).toMatchSnapshot(); 554 | }); 555 | 556 | it("should generate union factory even when querying one type from the union", async () => { 557 | const schema = buildSchema(/* GraphQL */ ` 558 | type Image { 559 | width: Int! 560 | height: Int! 561 | } 562 | 563 | type Audio { 564 | length: Int! 565 | } 566 | 567 | union Media = Image | Audio 568 | 569 | type Query { 570 | medias: [Media!]! 571 | } 572 | `); 573 | const ast = parse(/* GraphQL */ ` 574 | query GetMedias { 575 | medias { 576 | ... on Audio { 577 | length 578 | } 579 | } 580 | } 581 | `); 582 | 583 | const output = await plugin( 584 | schema, 585 | [{ location: "GetMedias.graphql", document: ast }], 586 | { schemaFactoriesPath: "./factories" } 587 | ); 588 | expect(output).toMatchSnapshot(); 589 | }); 590 | 591 | it("should support custom root Query", async () => { 592 | const schema = buildSchema(/* GraphQL */ ` 593 | type User { 594 | username: String! 595 | } 596 | 597 | type CustomQuery { 598 | user: User 599 | } 600 | 601 | schema { 602 | query: CustomQuery 603 | } 604 | `); 605 | const ast = parse(/* GraphQL */ ` 606 | query GetUser { 607 | user { 608 | username 609 | } 610 | } 611 | `); 612 | 613 | const output = await plugin( 614 | schema, 615 | [{ location: "GetUser.graphql", document: ast }], 616 | { schemaFactoriesPath: "./factories" } 617 | ); 618 | expect(output).toMatchSnapshot(); 619 | }); 620 | }); 621 | -------------------------------------------------------------------------------- /packages/graphql-codegen-factories/src/operations/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./plugin"; 2 | -------------------------------------------------------------------------------- /packages/graphql-codegen-factories/src/operations/plugin.ts: -------------------------------------------------------------------------------- 1 | import { Kind, FragmentDefinitionNode, concatAST, DocumentNode } from "graphql"; 2 | import { 3 | oldVisit, 4 | PluginFunction, 5 | Types, 6 | } from "@graphql-codegen/plugin-helpers"; 7 | import { 8 | FactoriesOperationsVisitor, 9 | FactoriesOperationsVisitorRawConfig, 10 | } from "./FactoriesOperationsVisitor"; 11 | 12 | export const plugin: PluginFunction< 13 | FactoriesOperationsVisitorRawConfig, 14 | Types.ComplexPluginOutput 15 | > = (schema, documents, config, info) => { 16 | const allAst = concatAST( 17 | documents.map(({ document }) => document as DocumentNode) 18 | ); 19 | const fragments = allAst.definitions.filter( 20 | (d) => d.kind === Kind.FRAGMENT_DEFINITION 21 | ) as FragmentDefinitionNode[]; 22 | const allFragments: FragmentDefinitionNode[] = [ 23 | ...fragments, 24 | // `externalFragments` config is passed by the near-operation-file preset. 25 | // It is an array of fragments declared outside of the operation file. 26 | ...(config.externalFragments || []).map(({ node }) => node), 27 | ]; 28 | 29 | const visitor = new FactoriesOperationsVisitor( 30 | schema, 31 | allFragments, 32 | config, 33 | info?.outputFile 34 | ); 35 | const content = ( 36 | oldVisit(allAst, { leave: visitor }) as DocumentNode 37 | ).definitions 38 | .filter((definition) => typeof definition === "string") 39 | .join("\n"); 40 | 41 | return { 42 | prepend: visitor.getImports(), 43 | content, 44 | }; 45 | }; 46 | -------------------------------------------------------------------------------- /packages/graphql-codegen-factories/src/schema/FactoriesSchemaVisitor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ObjectTypeDefinitionNode, 3 | FieldDefinitionNode, 4 | NamedTypeNode, 5 | NonNullTypeNode, 6 | InputObjectTypeDefinitionNode, 7 | InputValueDefinitionNode, 8 | UnionTypeDefinitionNode, 9 | ListTypeNode, 10 | GraphQLEnumType, 11 | GraphQLUnionType, 12 | GraphQLInterfaceType, 13 | GraphQLObjectType, 14 | GraphQLSchema, 15 | isEnumType, 16 | isUnionType, 17 | isInterfaceType, 18 | isObjectType, 19 | } from "graphql"; 20 | import { 21 | DeclarationBlock, 22 | getConfigValue, 23 | indent, 24 | } from "@graphql-codegen/visitor-plugin-common"; 25 | import { 26 | FactoriesBaseVisitor, 27 | FactoriesBaseVisitorParsedConfig, 28 | FactoriesBaseVisitorRawConfig, 29 | } from "../FactoriesBaseVisitor"; 30 | 31 | export interface FactoriesSchemaVisitorRawConfig 32 | extends FactoriesBaseVisitorRawConfig { 33 | scalarDefaults?: Record; 34 | 35 | // the typescript plugin's options that we need to support explicitly: 36 | enumsAsTypes?: boolean; 37 | 38 | // injected by near-operation-file and import-types presets: 39 | namespacedImportName?: string; 40 | 41 | // the "import * as Types" statement is not injected by near-operation-file and import-types 42 | // without a list of documents, as reported in: https://github.com/dotansimha/graphql-code-generator/issues/5775 43 | // until this is fixed, the following option prepends it and fills namespacedImportName 44 | // 45 | // typesPath is also the name of the option used by the import-types presets 46 | // near-operation-file uses baseTypesPath but that's because it generates multiple files 47 | // and it needs to generate relative paths 48 | typesPath?: string; 49 | 50 | // the following option does the same thing as namespacedImportName 51 | // but it is injected automatically while this one is provided by the user 52 | importTypesNamespace?: string; 53 | 54 | maybeValueDefault?: string; 55 | inputMaybeValueDefault?: string; 56 | disableDescriptions?: boolean; 57 | } 58 | 59 | export interface FactoriesSchemaVisitorParsedConfig 60 | extends FactoriesBaseVisitorParsedConfig { 61 | enumsAsTypes: boolean; 62 | scalarDefaults: Record; 63 | namespacedImportName: string | null; 64 | typesPath?: string; 65 | importTypesNamespace?: string; 66 | maybeValueDefault: string; 67 | inputMaybeValueDefault: string; 68 | disableDescriptions: boolean; 69 | } 70 | 71 | interface VisitedTypeNode { 72 | typename: string; 73 | defaultValue: string; 74 | isNullable: boolean; 75 | } 76 | 77 | interface UnvisitedFieldDefinitionNode 78 | extends Omit { 79 | type: VisitedTypeNode; 80 | } 81 | 82 | interface UnvisitedInputValueDefinitionNode 83 | extends Omit { 84 | type: VisitedTypeNode; 85 | } 86 | 87 | interface UnvisitedNonNullTypeNode extends Omit { 88 | type: VisitedTypeNode; 89 | } 90 | 91 | interface UnvisitedUnionTypeDefinitionNode 92 | extends Omit { 93 | types: VisitedTypeNode[]; 94 | } 95 | 96 | interface UnvisitedListTypeNode extends Omit { 97 | type: VisitedTypeNode; 98 | } 99 | 100 | export class FactoriesSchemaVisitor extends FactoriesBaseVisitor< 101 | FactoriesSchemaVisitorRawConfig, 102 | FactoriesSchemaVisitorParsedConfig 103 | > { 104 | protected enums: Record; 105 | protected unions: Record; 106 | protected interfaces: Record< 107 | string, 108 | { 109 | interface: GraphQLInterfaceType | null; 110 | implementations: GraphQLObjectType[]; 111 | } 112 | >; 113 | 114 | constructor(schema: GraphQLSchema, config: FactoriesSchemaVisitorRawConfig) { 115 | const maybeValueDefault = getConfigValue(config.maybeValueDefault, "null"); 116 | const inputMaybeValueDefault = getConfigValue( 117 | config.inputMaybeValueDefault, 118 | maybeValueDefault 119 | ); 120 | const parsedConfig = { 121 | enumsAsTypes: getConfigValue(config.enumsAsTypes, false), 122 | scalarDefaults: getConfigValue(config.scalarDefaults, {}), 123 | namespacedImportName: getConfigValue( 124 | config.namespacedImportName, 125 | undefined 126 | ), 127 | typesPath: getConfigValue(config.typesPath, undefined), 128 | importTypesNamespace: getConfigValue( 129 | config.importTypesNamespace, 130 | undefined 131 | ), 132 | maybeValueDefault, 133 | inputMaybeValueDefault, 134 | disableDescriptions: getConfigValue(config.disableDescriptions, false), 135 | } as FactoriesSchemaVisitorParsedConfig; 136 | 137 | if (parsedConfig.typesPath && parsedConfig.namespacedImportName == null) { 138 | parsedConfig.namespacedImportName = 139 | parsedConfig.importTypesNamespace ?? "Types"; 140 | } 141 | 142 | super(config, parsedConfig); 143 | 144 | this.enums = {}; 145 | this.unions = {}; 146 | this.interfaces = {}; 147 | 148 | const initializeInterface = (name: string) => { 149 | if (this.interfaces[name] == null) { 150 | this.interfaces[name] = { 151 | interface: null, 152 | implementations: [], 153 | }; 154 | } 155 | }; 156 | Object.values(schema.getTypeMap()).forEach((type) => { 157 | if (isEnumType(type)) { 158 | this.enums[type.name] = type; 159 | } 160 | 161 | if (isUnionType(type)) { 162 | this.unions[type.name] = type; 163 | } 164 | 165 | if (isInterfaceType(type)) { 166 | initializeInterface(type.name); 167 | this.interfaces[type.name].interface = type; 168 | } 169 | 170 | if (isObjectType(type)) { 171 | type.getInterfaces().forEach((inter) => { 172 | initializeInterface(inter.name); 173 | this.interfaces[inter.name].implementations.push(type); 174 | }); 175 | } 176 | }); 177 | } 178 | 179 | public getImports() { 180 | const imports: string[] = []; 181 | 182 | if (this.config.typesPath) { 183 | imports.push( 184 | `import * as ${this.config.namespacedImportName} from '${this.config.typesPath}';\n` 185 | ); 186 | } 187 | 188 | return imports; 189 | } 190 | 191 | protected convertNameWithTypesNamespace(name: string) { 192 | return this.convertNameWithNamespace( 193 | name, 194 | this.config.namespacedImportName ?? undefined 195 | ); 196 | } 197 | 198 | protected convertObjectType( 199 | node: ObjectTypeDefinitionNode | InputObjectTypeDefinitionNode 200 | ): string { 201 | return new DeclarationBlock(this._declarationBlockConfig) 202 | .export() 203 | .asKind("function") 204 | .withName( 205 | `${this.convertFactoryName( 206 | node 207 | )}(props: Partial<${this.convertNameWithTypesNamespace( 208 | node.name.value 209 | )}> = {}): ${this.convertNameWithTypesNamespace(node.name.value)}` 210 | ) 211 | .withComment(node.description ?? null, this.config.disableDescriptions) 212 | .withBlock( 213 | [ 214 | indent("return {"), 215 | node.kind === "ObjectTypeDefinition" 216 | ? indent(indent(`__typename: "${node.name.value}",`)) 217 | : null, 218 | ...(node.fields ?? []), 219 | indent(indent("...props,")), 220 | indent("};"), 221 | ] 222 | .filter(Boolean) 223 | .join("\n") 224 | ).string; 225 | } 226 | 227 | protected convertNullableDefaultValue( 228 | nullableDefaultValue: string, 229 | defaultValue: string 230 | ) { 231 | return nullableDefaultValue.replace("{defaultValue}", defaultValue); 232 | } 233 | 234 | protected convertField( 235 | node: UnvisitedFieldDefinitionNode | UnvisitedInputValueDefinitionNode, 236 | nullableDefaultValue: string 237 | ): string { 238 | const { defaultValue, isNullable } = node.type; 239 | return indent( 240 | indent( 241 | `${node.name.value}: ${ 242 | isNullable 243 | ? this.convertNullableDefaultValue( 244 | nullableDefaultValue, 245 | defaultValue 246 | ) 247 | : defaultValue 248 | },` 249 | ) 250 | ); 251 | } 252 | 253 | protected getDefaultValue(nodeName: string): string { 254 | const scalarName = 255 | nodeName in this.unions 256 | ? // Take the first type from an union 257 | this.unions[nodeName].getTypes()[0].name 258 | : nodeName in this.interfaces 259 | ? // Take the first implementation from an interface 260 | this.interfaces[nodeName].implementations[0].name 261 | : nodeName; 262 | 263 | if (scalarName in this.config.scalarDefaults) { 264 | return this.config.scalarDefaults[scalarName]; 265 | } 266 | 267 | switch (scalarName) { 268 | case "Int": 269 | case "Float": 270 | return "0"; 271 | case "ID": 272 | case "String": 273 | return '""'; 274 | case "Boolean": 275 | return "false"; 276 | default: { 277 | if (scalarName in this.enums) { 278 | return this.config.enumsAsTypes 279 | ? `"${this.enums[scalarName].getValues()[0].value}"` 280 | : `${this.convertNameWithTypesNamespace( 281 | scalarName 282 | )}.${this.convertName( 283 | this.enums[scalarName].getValues()[0].name, 284 | { 285 | transformUnderscore: true, 286 | } 287 | )}`; 288 | } 289 | 290 | return `${this.convertFactoryName(scalarName)}({})`; 291 | } 292 | } 293 | } 294 | 295 | NamedType(node: NamedTypeNode): VisitedTypeNode { 296 | return { 297 | typename: node.name.value, 298 | defaultValue: this.getDefaultValue(node.name.value), 299 | isNullable: true, 300 | }; 301 | } 302 | 303 | ListType(node: UnvisitedListTypeNode): VisitedTypeNode { 304 | return { 305 | typename: node.type.typename, 306 | defaultValue: "[]", 307 | isNullable: true, 308 | }; 309 | } 310 | 311 | NonNullType(node: UnvisitedNonNullTypeNode): VisitedTypeNode { 312 | return { 313 | ...node.type, 314 | isNullable: false, 315 | }; 316 | } 317 | 318 | FieldDefinition(node: UnvisitedFieldDefinitionNode): string { 319 | return this.convertField(node, this.config.maybeValueDefault); 320 | } 321 | 322 | InputObjectTypeDefinition(node: InputObjectTypeDefinitionNode): string { 323 | return this.convertObjectType(node); 324 | } 325 | 326 | InputValueDefinition(node: UnvisitedInputValueDefinitionNode): string { 327 | return this.convertField(node, this.config.inputMaybeValueDefault); 328 | } 329 | 330 | ObjectTypeDefinition(node: ObjectTypeDefinitionNode): string | undefined { 331 | return this.convertObjectType(node); 332 | } 333 | 334 | UnionTypeDefinition( 335 | node: UnvisitedUnionTypeDefinitionNode 336 | ): string | undefined { 337 | const types = node.types ?? []; 338 | 339 | if (types.length <= 0) { 340 | // Creating an union that represents nothing is valid 341 | // So this is valid: 342 | // union Humanoid = Human | Droid 343 | // But this is also valid: 344 | // union Humanoid 345 | return undefined; 346 | } 347 | 348 | return new DeclarationBlock(this._declarationBlockConfig) 349 | .export() 350 | .asKind("function") 351 | .withName( 352 | `${this.convertFactoryName( 353 | node.name.value 354 | )}(props: Partial<${this.convertNameWithTypesNamespace( 355 | node.name.value 356 | )}> = {}): ${this.convertNameWithTypesNamespace(node.name.value)}` 357 | ) 358 | .withBlock( 359 | [ 360 | indent("switch(props.__typename) {"), 361 | ...types.flatMap((type) => [ 362 | indent(indent(`case "${type.typename}":`)), 363 | indent( 364 | indent( 365 | indent( 366 | `return ${this.convertFactoryName(type.typename)}(props);` 367 | ) 368 | ) 369 | ), 370 | ]), 371 | indent(indent(`case undefined:`)), 372 | indent(indent(`default:`)), 373 | indent( 374 | indent( 375 | indent( 376 | `return ${this.convertFactoryName( 377 | node.name.value 378 | )}({ __typename: "${types[0].typename}", ...props });` 379 | ) 380 | ) 381 | ), 382 | indent("}"), 383 | ] 384 | .filter(Boolean) 385 | .join("\n") 386 | ).string; 387 | } 388 | } 389 | -------------------------------------------------------------------------------- /packages/graphql-codegen-factories/src/schema/__tests__/__snapshots__/plugin.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`plugin should add descriptions 1`] = ` 4 | Object { 5 | "content": "/** This is a description with /* characters that need to be escaped *\\\\/. */ 6 | export function createPostMock(props: Partial = {}): Post { 7 | return { 8 | __typename: \\"Post\\", 9 | title: null, 10 | ...props, 11 | }; 12 | } 13 | 14 | /** This is a description for an input. */ 15 | export function createPostInputMock(props: Partial = {}): PostInput { 16 | return { 17 | title: null, 18 | ...props, 19 | }; 20 | } 21 | ", 22 | "prepend": Array [], 23 | } 24 | `; 25 | 26 | exports[`plugin should create factories for Query and Mutation 1`] = ` 27 | Object { 28 | "content": "export function createUserMock(props: Partial = {}): User { 29 | return { 30 | __typename: \\"User\\", 31 | id: \\"\\", 32 | ...props, 33 | }; 34 | } 35 | 36 | export function createQueryMock(props: Partial = {}): Query { 37 | return { 38 | __typename: \\"Query\\", 39 | users: [], 40 | ...props, 41 | }; 42 | } 43 | 44 | export function createMutationMock(props: Partial = {}): Mutation { 45 | return { 46 | __typename: \\"Mutation\\", 47 | createUser: createUserMock({}), 48 | ...props, 49 | }; 50 | } 51 | ", 52 | "prepend": Array [], 53 | } 54 | `; 55 | 56 | exports[`plugin should create factories for inputs 1`] = ` 57 | Object { 58 | "content": "export function createPostInputMock(props: Partial = {}): PostInput { 59 | return { 60 | id: null, 61 | title: \\"\\", 62 | ...props, 63 | }; 64 | } 65 | ", 66 | "prepend": Array [], 67 | } 68 | `; 69 | 70 | exports[`plugin should create factories for unions 1`] = ` 71 | Object { 72 | "content": "export function createUserMock(props: Partial = {}): User { 73 | return { 74 | __typename: \\"User\\", 75 | firstName: \\"\\", 76 | lastName: \\"\\", 77 | ...props, 78 | }; 79 | } 80 | 81 | export function createDroidMock(props: Partial = {}): Droid { 82 | return { 83 | __typename: \\"Droid\\", 84 | codeName: \\"\\", 85 | ...props, 86 | }; 87 | } 88 | 89 | export function createHumanoidMock(props: Partial = {}): Humanoid { 90 | switch(props.__typename) { 91 | case \\"User\\": 92 | return createUserMock(props); 93 | case \\"Droid\\": 94 | return createDroidMock(props); 95 | case undefined: 96 | default: 97 | return createHumanoidMock({ __typename: \\"User\\", ...props }); 98 | } 99 | } 100 | ", 101 | "prepend": Array [], 102 | } 103 | `; 104 | 105 | exports[`plugin should create factories with built-in types 1`] = ` 106 | Object { 107 | "content": "export function createUserMock(props: Partial = {}): User { 108 | return { 109 | __typename: \\"User\\", 110 | id: \\"\\", 111 | organizationId: null, 112 | email: \\"\\", 113 | name: null, 114 | age: 0, 115 | followers: null, 116 | geo: createGeoMock({}), 117 | isUser: false, 118 | isAdmin: null, 119 | status: UserStatus.Activated, 120 | favouriteFruit: null, 121 | posts: [], 122 | subscribers: null, 123 | ...props, 124 | }; 125 | } 126 | 127 | export function createGeoMock(props: Partial = {}): Geo { 128 | return { 129 | __typename: \\"Geo\\", 130 | lat: 0, 131 | lon: null, 132 | ...props, 133 | }; 134 | } 135 | 136 | export function createPostMock(props: Partial = {}): Post { 137 | return { 138 | __typename: \\"Post\\", 139 | id: \\"\\", 140 | ...props, 141 | }; 142 | } 143 | ", 144 | "prepend": Array [], 145 | } 146 | `; 147 | 148 | exports[`plugin should customize the factory name 1`] = ` 149 | Object { 150 | "content": "export function newUser(props: Partial = {}): User { 151 | return { 152 | __typename: \\"User\\", 153 | id: \\"\\", 154 | ...props, 155 | }; 156 | } 157 | ", 158 | "prepend": Array [], 159 | } 160 | `; 161 | 162 | exports[`plugin should customize the input maybe value default 1`] = ` 163 | Object { 164 | "content": "export function createPostInputMock(props: Partial = {}): PostInput { 165 | return { 166 | title: undefined, 167 | ...props, 168 | }; 169 | } 170 | ", 171 | "prepend": Array [], 172 | } 173 | `; 174 | 175 | exports[`plugin should customize the maybe value default 1`] = ` 176 | Object { 177 | "content": "export function createPostMock(props: Partial = {}): Post { 178 | return { 179 | __typename: \\"Post\\", 180 | title: undefined, 181 | ...props, 182 | }; 183 | } 184 | 185 | export function createPostInputMock(props: Partial = {}): PostInput { 186 | return { 187 | title: undefined, 188 | ...props, 189 | }; 190 | } 191 | ", 192 | "prepend": Array [], 193 | } 194 | `; 195 | 196 | exports[`plugin should customize the maybe value default and input maybe value default independently 1`] = ` 197 | Object { 198 | "content": "export function createPostMock(props: Partial = {}): Post { 199 | return { 200 | __typename: \\"Post\\", 201 | title: undefined, 202 | ...props, 203 | }; 204 | } 205 | 206 | export function createPostInputMock(props: Partial = {}): PostInput { 207 | return { 208 | title: null, 209 | ...props, 210 | }; 211 | } 212 | ", 213 | "prepend": Array [], 214 | } 215 | `; 216 | 217 | exports[`plugin should customize the maybe value default with default value 1`] = ` 218 | Object { 219 | "content": "export function createPostMock(props: Partial = {}): Post { 220 | return { 221 | __typename: \\"Post\\", 222 | author: createPostAuthorMock({}), 223 | ...props, 224 | }; 225 | } 226 | 227 | export function createPostAuthorMock(props: Partial = {}): PostAuthor { 228 | return { 229 | __typename: \\"PostAuthor\\", 230 | username: \\"\\", 231 | ...props, 232 | }; 233 | } 234 | 235 | export function createPostInputMock(props: Partial = {}): PostInput { 236 | return { 237 | author: createPostAuthorMock({}), 238 | ...props, 239 | }; 240 | } 241 | ", 242 | "prepend": Array [], 243 | } 244 | `; 245 | 246 | exports[`plugin should disable descriptions 1`] = ` 247 | Object { 248 | "content": "export function createPostMock(props: Partial = {}): Post { 249 | return { 250 | __typename: \\"Post\\", 251 | title: null, 252 | ...props, 253 | }; 254 | } 255 | 256 | export function createPostInputMock(props: Partial = {}): PostInput { 257 | return { 258 | title: null, 259 | ...props, 260 | }; 261 | } 262 | ", 263 | "prepend": Array [], 264 | } 265 | `; 266 | 267 | exports[`plugin should import types from other file 1`] = ` 268 | Object { 269 | "content": "export function createUserMock(props: Partial = {}): SharedTypes.User { 270 | return { 271 | __typename: \\"User\\", 272 | id: \\"\\", 273 | ...props, 274 | }; 275 | } 276 | ", 277 | "prepend": Array [ 278 | "import * as SharedTypes from './types.ts'; 279 | ", 280 | ], 281 | } 282 | `; 283 | 284 | exports[`plugin should support directives 1`] = ` 285 | Object { 286 | "content": "export function createUserMock(props: Partial = {}): User { 287 | return { 288 | __typename: \\"User\\", 289 | id: null, 290 | ...props, 291 | }; 292 | } 293 | ", 294 | "prepend": Array [], 295 | } 296 | `; 297 | 298 | exports[`plugin should support enums with an underscore 1`] = ` 299 | Object { 300 | "content": "export function createUserMock(props: Partial = {}): User { 301 | return { 302 | __typename: \\"User\\", 303 | role: UserRole.SuperAdmin, 304 | ...props, 305 | }; 306 | } 307 | ", 308 | "prepend": Array [], 309 | } 310 | `; 311 | 312 | exports[`plugin should support interfaces 1`] = ` 313 | Object { 314 | "content": "export function createUserMock(props: Partial = {}): User { 315 | return { 316 | __typename: \\"User\\", 317 | id: \\"\\", 318 | username: \\"\\", 319 | ...props, 320 | }; 321 | } 322 | ", 323 | "prepend": Array [], 324 | } 325 | `; 326 | 327 | exports[`plugin should use enums as types 1`] = ` 328 | Object { 329 | "content": "export function createUserMock(props: Partial = {}): User { 330 | return { 331 | __typename: \\"User\\", 332 | status: \\"Activated\\", 333 | ...props, 334 | }; 335 | } 336 | ", 337 | "prepend": Array [], 338 | } 339 | `; 340 | 341 | exports[`plugin should use the custom scalar defaults 1`] = ` 342 | Object { 343 | "content": "export function createUserMock(props: Partial = {}): User { 344 | return { 345 | __typename: \\"User\\", 346 | createdAt: new Date(), 347 | ...props, 348 | }; 349 | } 350 | ", 351 | "prepend": Array [], 352 | } 353 | `; 354 | -------------------------------------------------------------------------------- /packages/graphql-codegen-factories/src/schema/__tests__/plugin.ts: -------------------------------------------------------------------------------- 1 | import { buildSchema } from "graphql"; 2 | import { plugin } from "../plugin"; 3 | 4 | describe("plugin", () => { 5 | it("should create factories with built-in types", async () => { 6 | const schema = buildSchema(/* GraphQL */ ` 7 | type User { 8 | id: ID! 9 | organizationId: ID 10 | email: String! 11 | name: String 12 | age: Int! 13 | followers: Int 14 | geo: Geo! 15 | isUser: Boolean! 16 | isAdmin: Boolean 17 | status: UserStatus! 18 | favouriteFruit: Fruit 19 | posts: [Post!]! 20 | subscribers: [User!] 21 | } 22 | 23 | type Geo { 24 | lat: Float! 25 | lon: Float 26 | } 27 | 28 | type Post { 29 | id: ID! 30 | } 31 | 32 | enum UserStatus { 33 | Activated 34 | Created 35 | } 36 | 37 | enum Fruit { 38 | Pineapple 39 | Mango 40 | } 41 | `); 42 | 43 | const output = await plugin(schema, [], {}); 44 | expect(output).toMatchSnapshot(); 45 | }); 46 | 47 | it("should use enums as types", async () => { 48 | const schema = buildSchema(/* GraphQL */ ` 49 | type User { 50 | status: UserStatus! 51 | } 52 | 53 | enum UserStatus { 54 | Activated 55 | Created 56 | } 57 | `); 58 | 59 | const output = await plugin(schema, [], { enumsAsTypes: true }); 60 | expect(output).toMatchSnapshot(); 61 | }); 62 | 63 | it("should use the custom scalar defaults", async () => { 64 | const schema = buildSchema(/* GraphQL */ ` 65 | type User { 66 | createdAt: Date! 67 | } 68 | 69 | scalar Date 70 | `); 71 | 72 | const output = await plugin(schema, [], { 73 | scalarDefaults: { Date: "new Date()" }, 74 | }); 75 | expect(output).toMatchSnapshot(); 76 | }); 77 | 78 | it("should create factories for inputs", async () => { 79 | const schema = buildSchema(/* GraphQL */ ` 80 | input PostInput { 81 | id: ID 82 | title: String! 83 | } 84 | `); 85 | 86 | const output = await plugin(schema, [], {}); 87 | expect(output).toMatchSnapshot(); 88 | }); 89 | 90 | it("should create factories for Query and Mutation", async () => { 91 | const schema = buildSchema(/* GraphQL */ ` 92 | type User { 93 | id: ID! 94 | } 95 | 96 | type Query { 97 | users: [User!]! 98 | } 99 | 100 | type Mutation { 101 | createUser(id: ID!): User! 102 | } 103 | `); 104 | 105 | const output = await plugin(schema, [], {}); 106 | expect(output).toMatchSnapshot(); 107 | }); 108 | 109 | it("should customize the factory name", async () => { 110 | const schema = buildSchema(/* GraphQL */ ` 111 | type User { 112 | id: ID! 113 | } 114 | `); 115 | 116 | const output = await plugin(schema, [], { factoryName: "new{Type}" }); 117 | expect(output).toMatchSnapshot(); 118 | }); 119 | 120 | it("should customize the maybe value default", async () => { 121 | const schema = buildSchema(/* GraphQL */ ` 122 | type Post { 123 | title: String 124 | } 125 | input PostInput { 126 | title: String 127 | } 128 | `); 129 | 130 | const output = await plugin(schema, [], { 131 | maybeValueDefault: "undefined", 132 | }); 133 | expect(output).toMatchSnapshot(); 134 | }); 135 | 136 | it("should customize the maybe value default and input maybe value default independently", async () => { 137 | const schema = buildSchema(/* GraphQL */ ` 138 | type Post { 139 | title: String 140 | } 141 | input PostInput { 142 | title: String 143 | } 144 | `); 145 | 146 | const output = await plugin(schema, [], { 147 | maybeValueDefault: "undefined", 148 | inputMaybeValueDefault: "null", 149 | }); 150 | expect(output).toMatchSnapshot(); 151 | }); 152 | 153 | it("should customize the maybe value default with default value", async () => { 154 | const schema = buildSchema(/* GraphQL */ ` 155 | type Post { 156 | author: PostAuthor 157 | } 158 | type PostAuthor { 159 | username: String 160 | } 161 | input PostInput { 162 | author: PostAuthor 163 | } 164 | `); 165 | 166 | const output = await plugin(schema, [], { 167 | maybeValueDefault: "{defaultValue}", 168 | }); 169 | expect(output).toMatchSnapshot(); 170 | }); 171 | 172 | it("should customize the input maybe value default", async () => { 173 | const schema = buildSchema(/* GraphQL */ ` 174 | input PostInput { 175 | title: String 176 | } 177 | `); 178 | 179 | const output = await plugin(schema, [], { 180 | inputMaybeValueDefault: "undefined", 181 | }); 182 | expect(output).toMatchSnapshot(); 183 | }); 184 | 185 | it("should support enums with an underscore", async () => { 186 | const schema = buildSchema(/* GraphQL */ ` 187 | enum UserRole { 188 | SUPER_ADMIN 189 | ADMIN 190 | } 191 | type User { 192 | role: UserRole! 193 | } 194 | `); 195 | 196 | const output = await plugin(schema, [], {}); 197 | expect(output).toMatchSnapshot(); 198 | }); 199 | 200 | it("should support directives", async () => { 201 | const schema = buildSchema(/* GraphQL */ ` 202 | directive @test on FIELD_DEFINITION 203 | 204 | type User { 205 | id: String 206 | } 207 | `); 208 | 209 | const output = await plugin(schema, [], {}); 210 | expect(output).toMatchSnapshot(); 211 | }); 212 | 213 | it("should import types from other file", async () => { 214 | const schema = buildSchema(/* GraphQL */ ` 215 | type User { 216 | id: ID! 217 | } 218 | `); 219 | 220 | const output = await plugin(schema, [], { 221 | typesPath: "./types.ts", 222 | importTypesNamespace: "SharedTypes", 223 | }); 224 | expect(output).toMatchSnapshot(); 225 | }); 226 | 227 | it("should create factories for unions", async () => { 228 | const schema = buildSchema(/* GraphQL */ ` 229 | type User { 230 | firstName: String! 231 | lastName: String! 232 | } 233 | 234 | type Droid { 235 | codeName: String! 236 | } 237 | 238 | union Humanoid = User | Droid 239 | `); 240 | 241 | const output = await plugin(schema, [], {}); 242 | expect(output).toMatchSnapshot(); 243 | }); 244 | 245 | it("should support interfaces", async () => { 246 | const schema = buildSchema(/* GraphQL */ ` 247 | interface Node { 248 | id: ID! 249 | } 250 | type User implements Node { 251 | id: ID! 252 | username: String! 253 | } 254 | `); 255 | 256 | const output = await plugin(schema, [], {}); 257 | expect(output).toMatchSnapshot(); 258 | }); 259 | 260 | it("should add descriptions", async () => { 261 | const schema = buildSchema(/* GraphQL */ ` 262 | """ 263 | This is a description with /* characters that need to be escaped */. 264 | """ 265 | type Post { 266 | title: String 267 | } 268 | 269 | """ 270 | This is a description for an input. 271 | """ 272 | input PostInput { 273 | title: String 274 | } 275 | `); 276 | 277 | const output = await plugin(schema, [], {}); 278 | expect(output).toMatchSnapshot(); 279 | }); 280 | 281 | it("should disable descriptions", async () => { 282 | const schema = buildSchema(/* GraphQL */ ` 283 | """ 284 | This is a description with /* characters that need to be escaped */. 285 | """ 286 | type Post { 287 | title: String 288 | } 289 | 290 | """ 291 | This is a description for an input. 292 | """ 293 | input PostInput { 294 | title: String 295 | } 296 | `); 297 | 298 | const output = await plugin(schema, [], { 299 | disableDescriptions: true, 300 | }); 301 | expect(output).toMatchSnapshot(); 302 | }); 303 | }); 304 | -------------------------------------------------------------------------------- /packages/graphql-codegen-factories/src/schema/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./plugin"; 2 | -------------------------------------------------------------------------------- /packages/graphql-codegen-factories/src/schema/plugin.ts: -------------------------------------------------------------------------------- 1 | import { printSchema, parse, DocumentNode } from "graphql"; 2 | import { 3 | oldVisit, 4 | PluginFunction, 5 | Types, 6 | } from "@graphql-codegen/plugin-helpers"; 7 | import { 8 | FactoriesSchemaVisitor, 9 | FactoriesSchemaVisitorRawConfig, 10 | } from "./FactoriesSchemaVisitor"; 11 | 12 | export const plugin: PluginFunction< 13 | FactoriesSchemaVisitorRawConfig, 14 | Types.ComplexPluginOutput 15 | > = (schema, documents, config) => { 16 | const printedSchema = printSchema(schema); 17 | const astNode = parse(printedSchema); 18 | 19 | const visitor = new FactoriesSchemaVisitor(schema, config); 20 | const content = ( 21 | oldVisit(astNode, { leave: visitor }) as DocumentNode 22 | ).definitions 23 | .filter((definition) => typeof definition === "string") 24 | .join("\n"); 25 | 26 | return { 27 | prepend: visitor.getImports(), 28 | content, 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /packages/graphql-codegen-factories/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/recommended/tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "build" 5 | }, 6 | "include": ["src"] 7 | } 8 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # graphql-codegen-factories 2 | 3 | ![](https://img.shields.io/github/license/zhouzi/graphql-codegen-factories?style=for-the-badge) ![](https://img.shields.io/github/actions/workflow/status/zhouzi/graphql-codegen-factories/.github/workflows/ci.yml?branch=main&style=for-the-badge) ![](https://img.shields.io/npm/v/graphql-codegen-factories?style=for-the-badge) 4 | 5 | `graphql-codegen-factories` is a plugin for [GraphQL Code Generator](https://www.graphql-code-generator.com/) that generates factories from a GraphQL schema and operations. 6 | The factories can then be used to mock data, e.g for testing or seeding a database. 7 | 8 | For example, given this GraphQL schema: 9 | 10 | ```graphql 11 | type User { 12 | id: ID! 13 | username: String! 14 | } 15 | ``` 16 | 17 | The following factory will be generated: 18 | 19 | ```typescript 20 | export type User = /* generated by @graphql-codegen/typescript */; 21 | 22 | export function createUserMock(props: Partial = {}): User { 23 | return { 24 | __typename: "User", 25 | id: "", 26 | username: "", 27 | ...props, 28 | }; 29 | } 30 | ``` 31 | 32 | It is also possible to generate factories from an operation, for example: 33 | 34 | ```graphql 35 | query GetUser { 36 | user { 37 | id 38 | username 39 | } 40 | } 41 | ``` 42 | 43 | Will result in the following factories: 44 | 45 | ```typescript 46 | export type GetUserQuery = /* generated by @graphql-codegen/typescript-operations */; 47 | 48 | export function createGetUserQueryMock(props: Partial = {}): GetUserQuery { 49 | return { 50 | __typename: "Query", 51 | user: createGetUserQueryMock_user({}), 52 | ...props, 53 | }; 54 | } 55 | 56 | export function createGetUserQueryMock_user(props: Partial = {}): GetUserQuery["user"] { 57 | return { 58 | __typename: "User", 59 | id: "", 60 | username: "", 61 | ...props, 62 | }; 63 | } 64 | ``` 65 | 66 | You can also use a fake data generator to generate realistic factories such as: 67 | 68 | ```typescript 69 | import { faker } from "@faker-js/faker"; 70 | 71 | export function createUserMock(props: Partial = {}): User { 72 | return { 73 | __typename: "User", 74 | id: faker.random.alphaNumeric(16), 75 | username: faker.lorem.word(), 76 | ...props, 77 | }; 78 | } 79 | ``` 80 | 81 | - [Documentation](https://gabinaureche.com/graphql-codegen-factories/) 82 | - [Examples](https://github.com/zhouzi/graphql-codegen-factories/tree/main/examples) 83 | - [Minimal](https://stackblitz.com/github/zhouzi/graphql-codegen-factories/tree/main/examples/minimal) 84 | - [Usage with near-operation-file-preset](https://stackblitz.com/github/zhouzi/graphql-codegen-factories/tree/main/examples/usage-with-near-operation-file-preset) 85 | - [Usage with faker](https://stackblitz.com/github/zhouzi/graphql-codegen-factories/tree/main/examples/usage-with-faker) 86 | 87 | ## Showcase 88 | 89 | - [yummy-recipes/yummy](https://github.com/yummy-recipes/yummy) 90 | 91 | Are you using this plugin? [Let us know!](https://github.com/zhouzi/graphql-codegen-factories/issues/new) 92 | 93 | ## Contributors 94 | 95 | [![](https://github.com/zhouzi.png?size=50)](https://github.com/zhouzi) 96 | [![](https://github.com/ertrzyiks.png?size=50)](https://github.com/ertrzyiks) 97 | [![](https://github.com/jongbelegen.png?size=50)](https://github.com/jongbelegen) 98 | --------------------------------------------------------------------------------