├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── babel.config.js ├── jest.config.js ├── package.json ├── src ├── index.ts └── visitor.ts ├── tests ├── documents │ ├── GetRepository.graphql │ └── RepositoryFragment.graphql ├── globalSetup.ts ├── integration.test.ts ├── setup.ts └── utils.ts ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | **/generated 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | 'jest': true, 5 | }, 6 | parser: '@typescript-eslint/parser', 7 | plugins: ['@typescript-eslint'], 8 | extends: [ 9 | 'eslint:recommended', 10 | 'plugin:@typescript-eslint/eslint-recommended', 11 | 'plugin:@typescript-eslint/recommended', 12 | ], 13 | rules: { 14 | '@typescript-eslint/no-explicit-any': 'off', 15 | '@typescript-eslint/explicit-module-boundary-types': 'off', 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | 16 | - name: Setup Node 17 | uses: actions/setup-node@v2 18 | with: 19 | node-version: '15' 20 | 21 | - uses: actions/cache@v2 22 | with: 23 | path: '**/node_modules' 24 | key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} 25 | 26 | - name: Install Yarn 27 | run: npm install -g yarn 28 | 29 | - name: Install node dependencies 30 | run: yarn install 31 | 32 | - name: Lint 33 | run: yarn lint 34 | 35 | - name: Type Check 36 | run: yarn typecheck 37 | 38 | - name: Test 39 | run: yarn test --collectCoverage 40 | 41 | - name: Upload Code Coverage 42 | run: bash <(curl -s https://codecov.io/bash) 43 | env: 44 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | generated/ 4 | tests/schema/ 5 | yarn-error.log 6 | coverage/ 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | tests 2 | babel.config.js 3 | jest.config.js 4 | tsconfig.json 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Suyeol Jeon (https://github.com/devxoul) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-codegen-typescript-fixtures 2 | 3 | [![npm version](https://badge.fury.io/js/graphql-codegen-typescript-fixtures.svg)](https://badge.fury.io/js/graphql-codegen-typescript-fixtures) 4 | [![CI](https://github.com/devxoul/graphql-codegen-typescript-fixtures/workflows/CI/badge.svg)](https://github.com/devxoul/graphql-codegen-typescript-fixtures/actions/workflows/ci.yml) 5 | 6 | A plugin for [graphql-code-generator](https://www.graphql-code-generator.com/) that generates TypeScript fixtures for testing. 7 | 8 | ## At a Glance 9 | 10 | ```tsx 11 | import fixture from './generated/graphql-fixtures.ts' 12 | 13 | const user = fixture('User') 14 | user.name // "" 15 | user.followers.totalCount // 0 16 | 17 | // with Immer.js 18 | const repo = fixture('Repository', repo => { 19 | repo.name = 'my-cool-stuff' 20 | repo.stargazerCount = 1234 21 | }) 22 | repo.name // "my-cool-stuff" 23 | repo.stargazerCount // 1234 24 | ``` 25 | 26 | ## Features 27 | 28 | * 🍭 Strongly typed. 29 | 30 | type-hints 31 | 32 | * 🧬 Built-in support for [Immer](https://github.com/immerjs/immer) integration. 33 | 34 | immer 35 | 36 | ## Installation 37 | 38 | * Using Yarn: 39 | ```console 40 | $ yarn add graphql-codegen-typescript-fixtures --dev 41 | ``` 42 | * Using npm: 43 | ```console 44 | $ npm install graphql-codegen-typescript-fixtures --dev 45 | ``` 46 | 47 | Add lines below in your graphql-codegen configuration file. Check out [Configuration](Configuration) section for more details. 48 | 49 | ```diff 50 | generates: 51 | src/generated/graphql.ts: 52 | plugins: 53 | - "typescript" 54 | - "typescript-operations" 55 | + src/generated/graphql-fixtures.ts: 56 | + plugins: 57 | + - graphql-codegen-typescript-fixtures 58 | 59 | config: 60 | scalars: 61 | Date: string 62 | DateTime: string 63 | + fixtures: 64 | + typeDefinitionModule: "path/to/graphql/types.ts" 65 | ``` 66 | 67 | ## Configuration 68 | 69 | ### typeDefinitionModule 70 | 71 | *(Required)* A path for the GraphQL type definition module. This value is used to import the GraphQL type definitions. 72 | 73 | For example: 74 | 75 | ```yaml 76 | config: 77 | fixtures: 78 | typeDefinitionModule: "@src/generated/graphql" 79 | ``` 80 | 81 | And the generated code will be: 82 | 83 | ```ts 84 | // src/generated/graphql-fixtures.ts 85 | import * as types from '@src/generated/graphql' 86 | ``` 87 | 88 | ### immer 89 | 90 | *(Optional)* Whether to generate [Immer](https://github.com/immerjs/immer) integration. 91 | 92 | For example: 93 | 94 | ```yaml 95 | config: 96 | fixtures: 97 | immer: true 98 | ``` 99 | 100 | Then the second parameter of `fixture()` will become available. 101 | 102 | ```ts 103 | fixture('User', user => { 104 | user.name = 'Suyeol Jeon' 105 | }) 106 | ``` 107 | 108 | ### scalarDefaults 109 | 110 | *(Optional)* The default values of scalar types. Note that the values are directly written to the TypeScript code so you need to wrap strings with quotes properly. 111 | 112 | For example: 113 | 114 | ```yaml 115 | config: 116 | fixtures: 117 | scalarDefaults: 118 | Date: "'2021-01-01'" 119 | DateTime: "'2021-01-01T00:00:00+00:00'" 120 | Timestamp: 1609426800 121 | ``` 122 | 123 | ## License 124 | 125 | This project is under MIT license. See the [LICENSE](LICENSE) file for more info. 126 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { 4 | 'targets': { 5 | 'node': 'current' 6 | } 7 | }], 8 | '@babel/preset-typescript', 9 | ], 10 | }; 11 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | globalSetup: '/tests/globalSetup.ts', 3 | watchPathIgnorePatterns: [ 4 | '/build/*', 5 | '/tests/generated/*', 6 | ], 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-codegen-typescript-fixtures", 3 | "version": "1.1.1", 4 | "description": "A plugin for graphql-code-generator that generates TypeScript fixtures for testing.", 5 | "main": "./build/index.js", 6 | "repository": "https://github.com/devxoul/graphql-codegen-typescript-fixtures.git", 7 | "author": "Suyeol Jeon ", 8 | "license": "MIT", 9 | "private": false, 10 | "scripts": { 11 | "clean": "rm -rf ./build ./tests/generated ./tests/schema", 12 | "build": "tsc", 13 | "typecheck": "yarn tsc -p tsconfig.json --noemit --skipLibCheck", 14 | "lint": "eslint . --ext .ts", 15 | "test": "jest" 16 | }, 17 | "devDependencies": { 18 | "@babel/preset-env": "^7.15.6", 19 | "@babel/preset-typescript": "^7.15.0", 20 | "@graphql-codegen/cli": "^2.6.2", 21 | "@graphql-codegen/typescript": "^2.4.5", 22 | "@types/dedent": "^0.7.0", 23 | "@types/jest": "^27.0.1", 24 | "@typescript-eslint/eslint-plugin": "^4.31.2", 25 | "@typescript-eslint/parser": "^4.31.2", 26 | "babel-jest": "^27.2.0", 27 | "eslint": "^7.32.0", 28 | "graphql": "^16.3.0", 29 | "immer": "^9.0.6", 30 | "jest": "^27.2.0", 31 | "js-yaml": "^4.1.0", 32 | "typescript": "^4.4.3" 33 | }, 34 | "dependencies": { 35 | "@graphql-codegen/schema-ast": "^2.2.0", 36 | "dedent": "^0.7.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent' 2 | import { GraphQLSchema, parse, printSchema, visit } from 'graphql' 3 | 4 | import { PluginFunction, Types } from '@graphql-codegen/plugin-helpers' 5 | 6 | import { FixturesVisitor } from './visitor' 7 | 8 | export const plugin: PluginFunction = ( 9 | schema, 10 | documents, 11 | config, 12 | ) => { 13 | const visitor = new FixturesVisitor(schema, config) 14 | return { 15 | prepend: getPrepend(visitor), 16 | content: getContent(schema, visitor), 17 | append: getAppend(visitor) 18 | } 19 | } 20 | 21 | const getPrepend = (visitor: FixturesVisitor) => { 22 | const prepend: string[] = [] 23 | 24 | if (visitor.config.immer) { 25 | prepend.push(`import produce, { Draft } from 'immer'`) 26 | } 27 | 28 | if (visitor.config.typeDefinitionModule) { 29 | prepend.push(`import * as types from '${visitor.config.typeDefinitionModule}'\n`) 30 | } 31 | 32 | prepend.push('const fixtureMap = {') 33 | return prepend 34 | } 35 | 36 | const getContent = (schema: GraphQLSchema, visitor: FixturesVisitor) => { 37 | const printedSchema = printSchema(schema) 38 | const astNode = parse(printedSchema) 39 | 40 | const content = visit(astNode, visitor) 41 | .definitions.filter(Boolean) 42 | .join('\n') 43 | return ' ' + content.split('\n').join('\n ') 44 | } 45 | 46 | const getAppend = (visitor: FixturesVisitor) => { 47 | const closingBracket = '}' 48 | const fixtureFunctionDefinition = getFixtureFunctionDefinition(visitor.config.immer) 49 | const exportDefaultStatement = 'export default fixture' 50 | 51 | const append = filterTruthy([ 52 | closingBracket, 53 | fixtureFunctionDefinition, 54 | exportDefaultStatement, 55 | ]) 56 | return append.map(line => line + '\n') 57 | } 58 | 59 | const getFixtureFunctionDefinition = (immer: boolean) => { 60 | const generics = filterTruthy([ 61 | 'Name extends keyof typeof fixtureMap', 62 | 'Fixture = ReturnType', 63 | ]) 64 | const parameters = filterTruthy([ 65 | 'name: Name', 66 | immer && 'recipe?: (draft: Draft) => Draft | void,', 67 | ]) 68 | const returnType = 'Fixture' 69 | const returnStatement = immer 70 | ? 'return recipe ? produce(fixture, recipe) : fixture' 71 | : 'return fixture' 72 | 73 | return dedent` 74 | const fixture = < 75 | ${generics.join(',\n ')} 76 | >( 77 | ${parameters.join(',\n ')} 78 | ): ${returnType} => { 79 | const fixture: Fixture = fixtureMap[name]() 80 | ${returnStatement} 81 | } 82 | ` 83 | } 84 | 85 | const filterTruthy = (array: (string | false | undefined | null)[]): string[] => { 86 | return array.filter((item): item is string => !!item) 87 | } 88 | -------------------------------------------------------------------------------- /src/visitor.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent' 2 | import { 3 | EnumTypeDefinitionNode, GraphQLSchema, InputObjectTypeDefinitionNode, InterfaceTypeDefinitionNode, 4 | NamedTypeNode, NameNode, ObjectTypeDefinitionNode, ScalarTypeDefinitionNode, TypeNode, 5 | UnionTypeDefinitionNode 6 | } from 'graphql' 7 | 8 | import { 9 | BaseVisitor, ParsedTypesConfig, RawTypesConfig 10 | } from '@graphql-codegen/visitor-plugin-common' 11 | 12 | export type FixturesVisitorRawConfig = RawTypesConfig & { 13 | fixtures?: { 14 | typeDefinitionModule?: string 15 | scalarDefaults?: Record 16 | immer?: boolean 17 | } 18 | } 19 | 20 | type FixturesVisitorParsedConfig = ParsedTypesConfig & { 21 | typeDefinitionModule?: string 22 | scalarDefaults: Record 23 | immer: boolean 24 | } 25 | 26 | export class FixturesVisitor extends BaseVisitor< 27 | FixturesVisitorRawConfig, 28 | FixturesVisitorParsedConfig 29 | > { 30 | private scalarMap: { [name: string]: string } 31 | 32 | constructor(schema: GraphQLSchema, config: FixturesVisitorRawConfig) { 33 | super(config, { 34 | typeDefinitionModule: config.fixtures?.typeDefinitionModule, 35 | scalarDefaults: config.fixtures?.scalarDefaults ?? {}, 36 | immer: config.fixtures?.immer ?? false, 37 | }) 38 | this.scalarMap = typeof config.scalars !== 'string' ? config.scalars ?? {} : {} 39 | } 40 | 41 | ScalarTypeDefinition(node: ScalarTypeDefinitionNode): string { 42 | const defaultValue = (() => { 43 | if (this.config.scalarDefaults[node.name.value]) { 44 | return this.config.scalarDefaults[node.name.value] 45 | } 46 | const scalarAlias = this.scalarMap[node.name.value] 47 | switch (scalarAlias) { 48 | case 'string': 49 | return '\'\'' 50 | case 'number': 51 | return '0' 52 | default: 53 | return '({})' 54 | } 55 | })() 56 | return dedent` 57 | ${node.name.value}(): ${this.getTypeDefinition('Scalars')}['${node.name.value}'] { 58 | return ${defaultValue} 59 | }, 60 | ` 61 | } 62 | 63 | ObjectTypeDefinition(node: ObjectTypeDefinitionNode): string { 64 | if (['Query', 'Mutation'].includes(node.name.value)) { 65 | return '' 66 | } 67 | return this.generateObjectFixture(node, { 68 | typename: true, 69 | }) 70 | } 71 | 72 | InterfaceTypeDefinition(node: InterfaceTypeDefinitionNode): string { 73 | return this.generateObjectFixture(node, { 74 | typename: false, 75 | }) 76 | } 77 | 78 | UnionTypeDefinition(node: UnionTypeDefinitionNode): string | null { 79 | const firstType = node.types?.[0] 80 | if (!firstType) { 81 | return null 82 | } 83 | return dedent` 84 | ${node.name.value}(): ${this.getTypeDefinition(node.name.value)} { 85 | return ${this.getNamedTypeDefaultValue(firstType)} 86 | }, 87 | ` 88 | } 89 | 90 | EnumTypeDefinition(node: EnumTypeDefinitionNode): string { 91 | return dedent` 92 | ${node.name.value}(): ${this.getTypeDefinition(node.name.value)} { 93 | return '${node.values?.[0].name.value}' as ${this.getTypeDefinition(node.name.value)} 94 | }, 95 | ` 96 | } 97 | 98 | InputObjectTypeDefinition(node: InputObjectTypeDefinitionNode): string { 99 | return this.generateObjectFixture(node, { 100 | typename: false, 101 | }) 102 | } 103 | 104 | DirectiveDefinition(): string | null { 105 | return null 106 | } 107 | 108 | private generateObjectFixture(node: ObjectDefinitionNodeCompatible, options: { 109 | typename: boolean, 110 | }) { 111 | const fields = node.fields ?? [] 112 | return dedent` 113 | ${node.name.value}(): ${this.getTypeDefinition(node.name.value)} { 114 | const fixture: Partial<${this.getTypeDefinition(node.name.value)}> = { 115 | ${options.typename ? `__typename: '${node.name.value}',` : ''} 116 | } 117 | ${fields.map(field => (`Object.defineProperties(fixture, { 118 | ${field.name.value}: { 119 | get: () => { 120 | const self = this as any 121 | const value = self.__resolved_${field.name.value} ?? ${this.getObjectFieldDefaultValue(field)} 122 | self.__resolved_${field.name.value} = value 123 | return value 124 | } 125 | }, 126 | __resolved_${field.name.value}: { 127 | value: undefined, 128 | writable: true, 129 | } 130 | })`)).join('\n ')} 131 | return fixture as ${this.getTypeDefinition(node.name.value)} 132 | }, 133 | ` 134 | } 135 | 136 | private getTypeDefinition(name: string) { 137 | return this.config.typeDefinitionModule 138 | ? `types.${this.convertName(name)}` 139 | : name 140 | } 141 | 142 | private getObjectFieldDefaultValue(field: FieldDefinitioinNodeCompatible) { 143 | switch (field.type.kind) { 144 | case 'NamedType': 145 | return 'undefined' 146 | 147 | case 'ListType': 148 | return 'undefined' 149 | 150 | case 'NonNullType': 151 | if (field.type.type.kind === 'ListType') { 152 | return '[]' 153 | } else if (field.type.type.kind === 'NamedType') { 154 | return this.getNamedTypeDefaultValue(field.type.type) 155 | } else { 156 | return null 157 | } 158 | 159 | default: 160 | return null 161 | } 162 | } 163 | 164 | private getNamedTypeDefaultValue(node: NamedTypeNode) { 165 | const typeName = node.name.value 166 | 167 | if (this.config.scalarDefaults[typeName]) { 168 | return this.config.scalarDefaults[typeName] 169 | } 170 | 171 | switch (typeName) { 172 | case 'ID': 173 | return '\'\'' 174 | 175 | case 'String': 176 | return '\'\'' 177 | 178 | case 'Int': 179 | return '0' 180 | 181 | case 'Float': 182 | return '0' 183 | 184 | case 'Boolean': 185 | return 'false' 186 | 187 | default: 188 | return `this.${typeName}()` 189 | } 190 | } 191 | } 192 | 193 | type ObjectDefinitionNodeCompatible = { 194 | name: NameNode, 195 | fields?: ReadonlyArray, 196 | } 197 | 198 | type FieldDefinitioinNodeCompatible = { 199 | name: NameNode 200 | type: TypeNode 201 | } 202 | -------------------------------------------------------------------------------- /tests/documents/GetRepository.graphql: -------------------------------------------------------------------------------- 1 | query GetRepository($owner: String!, $name: String!) { 2 | repository(owner: $owner, name: $name) { 3 | ...RepositoryFragment 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/documents/RepositoryFragment.graphql: -------------------------------------------------------------------------------- 1 | fragment RepositoryFragment on Repository { 2 | name 3 | owner { 4 | login 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tests/globalSetup.ts: -------------------------------------------------------------------------------- 1 | import setup from './setup' 2 | import { downloadFile, execAsync } from './utils' 3 | 4 | export default async () => { 5 | await clean() 6 | await buildPlugin() 7 | await makeDirectories() 8 | await downloadSchema() 9 | await generateFixturesForTyping() 10 | await generateFixturesForTypingWithImmer() 11 | } 12 | 13 | const clean = async () => { 14 | await execAsync('yarn clean') 15 | } 16 | 17 | const buildPlugin = async () => { 18 | await execAsync('yarn build') 19 | } 20 | 21 | const makeDirectories = async () => { 22 | await execAsync(`mkdir -p ./tests/schema`) 23 | await execAsync(`mkdir -p ./tests/generated`) 24 | } 25 | 26 | 27 | const downloadSchema = async () => { 28 | const url = 'https://docs.github.com/public/schema.docs.graphql' 29 | const destination = './tests/schema/github.schema.graphql' 30 | await downloadFile(url, destination) 31 | } 32 | 33 | const generateFixturesForTyping = async () => { 34 | await setup({ 35 | suffix: 'for-typing', 36 | ignoreErrors: true, 37 | }) 38 | } 39 | 40 | const generateFixturesForTypingWithImmer = async () => { 41 | await setup({ 42 | suffix: 'for-typing-with-immer', 43 | ignoreErrors: true, 44 | fixtureConfig: { 45 | immer: true, 46 | }, 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /tests/integration.test.ts: -------------------------------------------------------------------------------- 1 | import setup, { Fixture } from './setup' 2 | import { execAsync } from './utils' 3 | 4 | jest.setTimeout(10000) 5 | 6 | let fixture: Fixture 7 | 8 | beforeAll(async () => { 9 | fixture = await setup({ 10 | fixtureConfig: { 11 | scalarDefaults: { 12 | URI: `'https://example.com'`, 13 | }, 14 | } 15 | }) 16 | }) 17 | 18 | it('passes type checks', async () => { 19 | await execAsync(`yarn tsc tests/generated/graphql-fixtures-for-typing.ts --noemit --skipLibCheck`) 20 | await execAsync(`yarn tsc tests/generated/graphql-fixtures-for-typing-with-immer.ts --noemit --skipLibCheck`) 21 | }) 22 | 23 | describe('Scalar', () => { 24 | describe('when the default value is not configured', () => { 25 | it('defaults to the empty object', async () => { 26 | expect(fixture('Base64String')).toEqual({}) 27 | expect(fixture('FileAddition').contents).toEqual({}) 28 | }) 29 | }) 30 | 31 | describe('when the default value is configured', () => { 32 | it('defaults to the pre-configured value', () => { 33 | expect(fixture('URI')).toEqual('https://example.com') 34 | expect(fixture('Issue').url).toEqual('https://example.com') 35 | }) 36 | }) 37 | }) 38 | 39 | describe('Object', () => { 40 | it('properties are empty by default', () => { 41 | const repository = fixture('Repository') 42 | expect(repository.name).toEqual('') 43 | expect(repository.owner.login).toEqual('') 44 | expect(repository.stargazerCount).toEqual(0) 45 | expect(repository.isEmpty).toEqual(false) 46 | expect(repository.homepageUrl).toBeUndefined() 47 | expect(repository.createdAt).toEqual('') 48 | expect(repository.url).toEqual('https://example.com') 49 | }) 50 | }) 51 | 52 | describe('Interface', () => { 53 | it('properties are empty by default', () => { 54 | const repositoryOwner = fixture('RepositoryOwner') 55 | expect(repositoryOwner.login).toEqual('') 56 | expect(repositoryOwner.avatarUrl).toEqual('https://example.com') 57 | }) 58 | }) 59 | 60 | describe('Union', () => { 61 | it('defaults to the first type', () => { 62 | const issueOrPullRequest = fixture('IssueOrPullRequest') 63 | expect(issueOrPullRequest.__typename).toEqual('Issue') 64 | }) 65 | }) 66 | 67 | describe('Enum', () => { 68 | it('defaults to the first value', () => { 69 | const issueState = fixture('IssueState') 70 | expect(issueState).toEqual('CLOSED') 71 | }) 72 | }) 73 | 74 | describe('InputObject', () => { 75 | it('properties are empty by default', () => { 76 | const createIssueInput = fixture('CreateIssueInput') 77 | expect(createIssueInput.title).toEqual('') 78 | expect(createIssueInput.body).toBeUndefined() 79 | }) 80 | }) 81 | 82 | describe('a fixture with immer', () => { 83 | it('properties can be configured', async () => { 84 | const fixture = await setup({ 85 | fixtureConfig: { 86 | immer: true, 87 | }, 88 | }) 89 | const repository = fixture('Repository', repo => { 90 | repo.name = 'graphql-codegen-typescript-fixtures' 91 | repo.owner.login = 'devxoul' 92 | repo.stargazerCount = 1234 93 | repo.homepageUrl = 'https://example.com' 94 | }) 95 | expect(repository.name).toEqual('graphql-codegen-typescript-fixtures') 96 | expect(repository.owner.login).toEqual('devxoul') 97 | expect(repository.stargazerCount).toEqual(1234) 98 | expect(repository.isEmpty).toEqual(false) 99 | expect(repository.homepageUrl).toEqual('https://example.com') 100 | }) 101 | }) 102 | 103 | it('typecheck', async () => { 104 | let error: Error | undefined 105 | try { 106 | await execAsync('yarn tsc tests/generated/graphql-fixtures-for-typing.ts --noemit --skipLibCheck') 107 | error = undefined 108 | } catch (err) { 109 | error = err 110 | } 111 | expect(error).toBeUndefined() 112 | }) 113 | -------------------------------------------------------------------------------- /tests/setup.ts: -------------------------------------------------------------------------------- 1 | import yaml from 'js-yaml' 2 | 3 | import { execAsync, getRandomString, writeFileAsync } from './utils' 4 | 5 | type Options = { 6 | suffix?: string 7 | fixtureConfig?: FixtureConfig 8 | ignoreErrors?: boolean 9 | } 10 | 11 | type FixtureConfig = { 12 | typeDefinitionModule?: string 13 | scalarDefaults?: Record 14 | immer?: boolean 15 | } 16 | 17 | type OptionsWithoutImmer = Options & ( 18 | { fixtureConfig: undefined } | { 19 | fixtureConfig: { 20 | immer: false 21 | } 22 | } 23 | ) 24 | type OptionsWithImmer = Options & { 25 | fixtureConfig: { 26 | immer: true 27 | } 28 | } 29 | 30 | export type Fixture = typeof import('./generated/graphql-fixtures-for-typing').default 31 | export type FixtureWithImmer = typeof import('./generated/graphql-fixtures-for-typing-with-immer').default 32 | 33 | async function setup(options?: OptionsWithoutImmer): Promise 34 | async function setup(options?: OptionsWithImmer): Promise 35 | async function setup(options?: Options): Promise 36 | 37 | async function setup(options?: Options): Promise { 38 | const suffix = options?.suffix || getRandomString() 39 | const configFilePath = `./tests/generated/codegen-${suffix}.yml` 40 | const fixtureModuleName = `graphql-fixtures-${suffix}` 41 | 42 | await writeConfig({ 43 | configFilePath, 44 | fixtureModuleName, 45 | additionalFixtureConfig: options?.fixtureConfig 46 | }) 47 | await runCodegen(configFilePath) 48 | 49 | const fixture = await safeImport({ 50 | fixtureModuleName, 51 | ignoreErrors: options.ignoreErrors, 52 | }) 53 | return fixture 54 | } 55 | 56 | export default setup 57 | 58 | 59 | type WriteConfigOptions = { 60 | configFilePath: string 61 | fixtureModuleName: string 62 | additionalFixtureConfig?: FixtureConfig 63 | } 64 | 65 | const writeConfig = async (options: WriteConfigOptions) => { 66 | const jsonConfig = { 67 | overwrite: true, 68 | schema: './tests/schema/github.schema.graphql', 69 | documents: './tests/documents/*.graphql', 70 | generates: { 71 | './tests/generated/graphql.ts': { 72 | plugins: [ 73 | 'typescript', 74 | ], 75 | }, 76 | [`./tests/generated/${options.fixtureModuleName}.ts`]: { 77 | plugins: [ 78 | './build/index.js', 79 | ], 80 | } 81 | }, 82 | config: { 83 | scalars: { 84 | Date: 'string', 85 | DateTime: 'string', 86 | URI: 'string', 87 | }, 88 | fixtures: { 89 | typeDefinitionModule: './graphql', 90 | ...(options.additionalFixtureConfig || {}), 91 | } 92 | }, 93 | } 94 | const yamlConfig = yaml.dump(jsonConfig, { 95 | quotingType: '"', 96 | forceQuotes: true, 97 | }) 98 | await writeFileAsync(options.configFilePath, yamlConfig) 99 | } 100 | 101 | 102 | const runCodegen = async (configFilePath: string) => { 103 | await execAsync(`yarn graphql-codegen --config ${configFilePath}`) 104 | } 105 | 106 | 107 | type SafeImportOptions = { 108 | fixtureModuleName: string 109 | ignoreErrors?: boolean 110 | } 111 | 112 | const safeImport = async (options: SafeImportOptions) => { 113 | try { 114 | const { default: fixture } = await import(`./generated/${options.fixtureModuleName}`) 115 | return fixture 116 | } catch (error) { 117 | if (!options.ignoreErrors) { 118 | throw error 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /tests/utils.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process' 2 | import fs from 'fs' 3 | import https from 'https' 4 | 5 | export const execAsync = async (command: string) => { 6 | return new Promise((resolve, reject) => { 7 | exec(command, (error) => { 8 | if (error) { 9 | reject(error) 10 | } else { 11 | resolve() 12 | } 13 | }) 14 | }) 15 | } 16 | 17 | export const downloadFile = async (url: string, destination: string) => { 18 | const file = fs.createWriteStream(destination) 19 | return new Promise((resolve) => { 20 | https.get(url, (response) => { 21 | response.pipe(file) 22 | resolve() 23 | }) 24 | }) 25 | } 26 | 27 | export const writeFileAsync = async (path: string, data: string) => { 28 | return new Promise((resolve, reject) => { 29 | fs.writeFile(path, data, (error) => { 30 | if (error) { 31 | reject(error) 32 | } else { 33 | resolve() 34 | } 35 | }) 36 | }) 37 | } 38 | 39 | export const getRandomString = () => { 40 | return Math.random().toString(36).substr(2, 5) 41 | } 42 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "commonjs", 5 | "lib": ["esnext"], 6 | "strict": true, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "outDir": "build", 10 | }, 11 | "exclude": [ 12 | "node_modules", 13 | "tests", 14 | ] 15 | } 16 | --------------------------------------------------------------------------------