├── .npmignore ├── src ├── flow │ ├── index.js │ ├── types.js │ └── language.js ├── scala │ ├── index.js │ ├── values.js │ ├── types.js │ ├── language.js │ └── naming.js ├── swift │ ├── index.ts │ ├── aws-scalar-helper.ts │ ├── s3Wrapper.ts │ ├── language.ts │ └── helpers.ts ├── typescript │ ├── index.ts │ ├── types.ts │ └── language.ts ├── flow-modern │ ├── index.ts │ ├── types │ │ ├── augment-babel-types.ts │ │ └── babel7.ts │ ├── helpers.ts │ ├── printer.ts │ ├── language.ts │ ├── __tests__ │ │ └── codeGeneration.ts │ └── codeGeneration.ts ├── polyfills.js ├── index.ts ├── utilities │ ├── array.ts │ ├── printing.ts │ ├── CodeGenerator.ts │ └── graphql.ts ├── compiler │ ├── visitors │ │ ├── inlineRedundantTypeConditions.ts │ │ ├── generateOperationId.ts │ │ ├── collectFragmentsReferenced.ts │ │ ├── collectAndMergeFields.ts │ │ └── typeCase.ts │ └── legacyIR.ts ├── printSchema.ts ├── introspectSchema.ts ├── downloadSchema.ts ├── validation.ts ├── errors.ts ├── serializeToJSON.ts ├── loading.ts ├── generate.ts └── cli.js ├── test ├── fixtures │ ├── starwars │ │ ├── AnonymousQuery.graphql │ │ ├── HeroName.graphql │ │ ├── TypenameAlias.graphql │ │ ├── ExplicitTypename.graphql │ │ ├── HeroAppearsIn.graphql │ │ ├── TwoHeroes.graphql │ │ ├── HeroAndFriendsNames.graphql │ │ ├── HeroAndFriends.graphql │ │ ├── gqlQueries.js │ │ └── schema.graphql │ └── misc │ │ └── schema.graphql ├── tsconfig.json ├── swift │ ├── __snapshots__ │ │ └── language.ts.snap │ ├── language.ts │ └── typeNameFromGraphQLType.ts ├── loading.ts ├── scala │ ├── __snapshots__ │ │ └── language.js.snap │ ├── language.js │ └── types.js ├── test-utils │ ├── helpers.ts │ └── matchers.ts ├── introspectSchema.ts ├── validation.ts ├── valueFromValueNode.ts ├── jsonOutput.ts ├── compiler │ └── visitors │ │ └── generateOperationId.ts ├── typescript │ └── codeGeneration.js └── flow │ └── codeGeneration.js ├── .github └── PULL_REQUEST_TEMPLATE.md ├── .travis.yml ├── tsconfig.json ├── NOTICE ├── CHANGELOG.md ├── LICENSE ├── package.json ├── CONTRIBUTING.md └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | -------------------------------------------------------------------------------- /src/flow/index.js: -------------------------------------------------------------------------------- 1 | export { generateSource } from './codeGeneration'; 2 | -------------------------------------------------------------------------------- /src/scala/index.js: -------------------------------------------------------------------------------- 1 | export { generateSource } from './codeGeneration'; 2 | -------------------------------------------------------------------------------- /src/swift/index.ts: -------------------------------------------------------------------------------- 1 | export { generateSource } from './codeGeneration'; 2 | -------------------------------------------------------------------------------- /src/typescript/index.ts: -------------------------------------------------------------------------------- 1 | export { generateSource } from './codeGeneration'; 2 | -------------------------------------------------------------------------------- /src/flow-modern/index.ts: -------------------------------------------------------------------------------- 1 | export { generateSource } from './codeGeneration'; 2 | -------------------------------------------------------------------------------- /test/fixtures/starwars/AnonymousQuery.graphql: -------------------------------------------------------------------------------- 1 | { 2 | hero { 3 | name 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/polyfills.js: -------------------------------------------------------------------------------- 1 | import 'core-js/fn/object/values'; 2 | import 'core-js/fn/object/entries'; 3 | -------------------------------------------------------------------------------- /test/fixtures/starwars/HeroName.graphql: -------------------------------------------------------------------------------- 1 | query HeroName { 2 | hero { 3 | name 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/starwars/TypenameAlias.graphql: -------------------------------------------------------------------------------- 1 | query HeroName { 2 | hero { 3 | __typename: name 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/starwars/ExplicitTypename.graphql: -------------------------------------------------------------------------------- 1 | query HeroName { 2 | hero { 3 | __typename 4 | name 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/starwars/HeroAppearsIn.graphql: -------------------------------------------------------------------------------- 1 | query HeroAppearsIn { 2 | hero { 3 | name 4 | appearsIn 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/starwars/TwoHeroes.graphql: -------------------------------------------------------------------------------- 1 | query TwoHeroes { 2 | r2: hero { 3 | name 4 | } 5 | luke: hero(episode: EMPIRE) { 6 | name 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/starwars/HeroAndFriendsNames.graphql: -------------------------------------------------------------------------------- 1 | query HeroAndFriendsNames($episode: Episode) { 2 | hero(episode: $episode) { 3 | name 4 | friends { 5 | name 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": [ 4 | "./**/*", 5 | "../src/**/*" 6 | ], 7 | "exclude": [ 8 | "./fixtures/**/*" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 7 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import './polyfills'; 2 | 3 | export { default as downloadSchema } from './downloadSchema'; 4 | export { default as introspectSchema } from './introspectSchema'; 5 | export { default as printSchema } from './printSchema'; 6 | export { default as generate } from './generate'; 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | # Allow Travis tests to run in containers. 4 | sudo: false 5 | 6 | node_js: 7 | - "8" 8 | 9 | cache: 10 | directories: 11 | - $HOME/.npm 12 | 13 | install: 14 | - npm install 15 | 16 | script: 17 | - npm test 18 | - npm run test:smoke 19 | -------------------------------------------------------------------------------- /test/fixtures/starwars/HeroAndFriends.graphql: -------------------------------------------------------------------------------- 1 | query HeroAndFriends($episode: Episode) { 2 | hero(episode: $episode) { 3 | ...heroDetails 4 | } 5 | } 6 | 7 | fragment heroDetails on Character { 8 | name 9 | ... on Droid { 10 | primaryFunction 11 | } 12 | ... on Human { 13 | height 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/flow-modern/types/augment-babel-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Augment incomplete definitions in `@types/babel-types` 3 | */ 4 | import 'babel-types'; 5 | 6 | declare module 'babel-types' { 7 | interface StringLiteralTypeAnnotation { 8 | value: string 9 | } 10 | 11 | interface ObjectTypeAnnotation { 12 | exact: boolean 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/swift/__snapshots__/language.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Swift code generation: Basic language constructs should handle multi-line descriptions 1`] = ` 4 | "/// A hero 5 | public struct Hero { 6 | /// A multiline comment 7 | /// on the hero's name. 8 | public var name: String 9 | /// A multiline comment 10 | /// on the hero's age. 11 | public var age: String 12 | }" 13 | `; 14 | -------------------------------------------------------------------------------- /src/flow-modern/types/babel7.ts: -------------------------------------------------------------------------------- 1 | // Suppress missing types for @babel/types and @babel/generator by 2 | // creating an alias for them to babel-types and babel-generator respectively. 3 | 4 | declare module '@babel/types' { 5 | export * from 'babel-types'; 6 | } 7 | declare module '@babel/generator' { 8 | export * from 'babel-generator'; 9 | 10 | import Generator from 'babel-generator'; 11 | export default Generator; 12 | } 13 | -------------------------------------------------------------------------------- /test/loading.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { loadAndMergeQueryDocuments } from '../src/loading'; 4 | 5 | describe('Validation', () => { 6 | test(`should extract gql snippet from javascript file`, () => { 7 | const inputPaths = [ 8 | path.join(__dirname, './fixtures/starwars/gqlQueries.js'), 9 | ]; 10 | 11 | const document = loadAndMergeQueryDocuments(inputPaths); 12 | 13 | expect(document).toMatchSnapshot(); 14 | }) 15 | }); 16 | -------------------------------------------------------------------------------- /test/scala/__snapshots__/language.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Scala code generation: Basic language constructs should handle multi-line descriptions 1`] = ` 4 | "/** 5 | * A hero 6 | */ 7 | case class Hero() { 8 | /** 9 | * A multiline comment 10 | * on the hero's name. 11 | */ 12 | val name: String = { 13 | } 14 | /** 15 | * A multiline comment 16 | * on the hero's age. 17 | */ 18 | val age: String = { 19 | } 20 | }" 21 | `; 22 | -------------------------------------------------------------------------------- /src/utilities/array.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | declare global { 4 | interface Array { 5 | flatMap(callbackfn: (value: T, index: number, array: T[]) => U[] | undefined, thisArg?: any): U[]; 6 | } 7 | } 8 | 9 | Object.defineProperty(Array.prototype, 'flatMap', { 10 | value: function(this: Array, callbackfn: (value: T, index: number, array: T[]) => U[], thisArg?: any): U[] { 11 | return [].concat.apply([], this.map(callbackfn, thisArg)); 12 | }, 13 | enumerable: false 14 | }); 15 | -------------------------------------------------------------------------------- /test/test-utils/helpers.ts: -------------------------------------------------------------------------------- 1 | import { parse, GraphQLSchema } from 'graphql'; 2 | import { compileToIR, CompilerOptions, } from '../../src/compiler'; 3 | import { loadSchema } from '../../src/loading'; 4 | 5 | export const starWarsSchema = loadSchema(require.resolve('../fixtures/starwars/schema.json')); 6 | 7 | export function compile(source: string, schema: GraphQLSchema = starWarsSchema, options: CompilerOptions = {}) { 8 | const document = parse(source); 9 | return compileToIR(schema, document, options); 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "outDir": "lib", 8 | "lib": ["es2017", "esnext.asynciterable", "dom"], 9 | "removeComments": true, 10 | "strict": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "noUnusedParameters": true, 14 | "noUnusedLocals": true, 15 | "allowJs": true, 16 | "allowUnreachableCode": true 17 | }, 18 | "include" : [ 19 | "src/**/*" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/swift/aws-scalar-helper.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLScalarType, 3 | } from 'graphql'; 4 | 5 | interface INameToValueMap { 6 | [key: string]: any; 7 | } 8 | 9 | export const awsScalarMap: INameToValueMap = { 10 | AWSDate: 'String', 11 | AWSTime: 'String', 12 | AWSDateTime: 'String', 13 | AWSTimestamp: 'Int', 14 | AWSEmail: 'String', 15 | AWSJSON: 'String', 16 | AWSURL: 'String', 17 | AWSPhone: 'String', 18 | AWSIPAddress: 'String', 19 | }; 20 | 21 | export function getTypeForAWSScalar(type: GraphQLScalarType): string { 22 | return awsScalarMap[type.name]; 23 | } -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | AWS AppSync Codegen for iOS 2 | 3 | This product includes software developed by Amazon Technologies, Inc (http://www.amazon.com/). 4 | 5 | Licensed under the MIT License. 6 | 7 | See the License for the specific language governing permissions and limitations under the License. 8 | 9 | ========================================================================= 10 | == Apollo GraphQL code generator == 11 | ========================================================================= 12 | 13 | This product includes the Apollo GraphQL code generator library licensed under the MIT license. 14 | -------------------------------------------------------------------------------- /test/introspectSchema.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { join } from 'path'; 3 | 4 | import { introspect } from '../src/introspectSchema'; 5 | 6 | describe('Introspecting GraphQL schema documents', () => { 7 | test(`should generate valid introspection JSON file`, async () => { 8 | const schemaContents = readFileSync(join(__dirname, './fixtures/starwars/schema.graphql')).toString(); 9 | const expected = readFileSync(join(__dirname, './fixtures/starwars/schema.json')).toString(); 10 | 11 | const schema = await introspect(schemaContents); 12 | 13 | expect(JSON.stringify(schema, null, 2)).toEqual(expected); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /test/fixtures/starwars/gqlQueries.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { gql, graphql } from 'react-apollo' 3 | 4 | const Query = gql` 5 | query HeroAndFriends($episode: Episode) { 6 | hero(episode: $episode) { 7 | ...heroDetails 8 | } 9 | } 10 | 11 | fragment heroDetails on Character { 12 | name 13 | ... on Droid { 14 | primaryFunction 15 | } 16 | ... on Human { 17 | height 18 | } 19 | } 20 | 21 | ${'this should be ignored'} 22 | ` 23 | 24 | const AnotherQuery = gql` 25 | query HeroName { 26 | hero { 27 | name 28 | } 29 | } 30 | ` 31 | 32 | function Component() { 33 | return
34 | } 35 | 36 | export default graphql(Query)(Component) 37 | -------------------------------------------------------------------------------- /src/compiler/visitors/inlineRedundantTypeConditions.ts: -------------------------------------------------------------------------------- 1 | import { SelectionSet, Selection } from '../'; 2 | 3 | export function inlineRedundantTypeConditions(selectionSet: SelectionSet): SelectionSet { 4 | const selections: Selection[] = []; 5 | 6 | for (const selection of selectionSet.selections) { 7 | if ( 8 | selection.kind === 'TypeCondition' && 9 | selectionSet.possibleTypes.every(type => selection.selectionSet.possibleTypes.includes(type)) 10 | ) { 11 | selections.push(...inlineRedundantTypeConditions(selection.selectionSet).selections); 12 | } else { 13 | selections.push(selection); 14 | } 15 | } 16 | 17 | return { 18 | possibleTypes: selectionSet.possibleTypes, 19 | selections 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/printSchema.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | 3 | import { buildClientSchema, printSchema } from 'graphql'; 4 | 5 | import { ToolError } from './errors' 6 | 7 | export default async function printSchemaFromIntrospectionResult(schemaPath: string, outputPath: string) { 8 | if (!fs.existsSync(schemaPath)) { 9 | throw new ToolError(`Cannot find GraphQL schema file: ${schemaPath}`); 10 | } 11 | 12 | const schemaJSON = JSON.parse(fs.readFileSync(schemaPath, 'utf8')); 13 | 14 | if (!schemaJSON.data) { 15 | throw new ToolError(`No introspection query result data found in: ${schemaPath}`); 16 | } 17 | 18 | const schema = buildClientSchema(schemaJSON.data); 19 | const schemaIDL = printSchema(schema); 20 | 21 | if (outputPath) { 22 | fs.writeFileSync(outputPath, schemaIDL); 23 | } else { 24 | console.log(schemaIDL); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log - AWS AppSync Codegen 2 | 3 | ## [Release 0.17.5](https://github.com/awslabs/aws-appsync-codegen/releases/tag/0.17.5) 4 | 5 | ### Enhancements 6 | 7 | * Fixes undeclared type errors when generating Swift enumerations, removed the Apollo module name as it doesn’t exist in this context. See [PR #14](https://github.com/awslabs/aws-appsync-codegen/pull/14) 8 | 9 | ## [Release 0.17.4](https://github.com/awslabs/aws-appsync-codegen/releases/tag/0.17.4) 10 | 11 | ### Enhancements 12 | 13 | * Update package dependencies 14 | 15 | ## [Release 0.17.3](https://github.com/awslabs/aws-appsync-codegen/releases/tag/0.17.3) 16 | 17 | ### New Features 18 | 19 | * Adds support for AWS AppSync Defined Scalars such as `AWSTimestamp`. 20 | 21 | ## [Release 0.17.2](https://github.com/awslabs/aws-appsync-codegen/releases/tag/0.17.2) 22 | 23 | ### New Features 24 | 25 | * Initial release -------------------------------------------------------------------------------- /src/introspectSchema.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | 3 | import { buildASTSchema, graphql, parse } from 'graphql'; 4 | import { introspectionQuery } from 'graphql/utilities'; 5 | 6 | import { ToolError } from './errors' 7 | 8 | export async function introspect(schemaContents: string) { 9 | const schema = buildASTSchema(parse(schemaContents)); 10 | return await graphql(schema, introspectionQuery); 11 | } 12 | 13 | export default async function introspectSchema(schemaPath: string, outputPath: string) { 14 | if (!fs.existsSync(schemaPath)) { 15 | throw new ToolError(`Cannot find GraphQL schema file: ${schemaPath}`); 16 | } 17 | 18 | const schemaContents = fs.readFileSync(schemaPath).toString(); 19 | const result = await introspect(schemaContents); 20 | 21 | if (result.errors) { 22 | throw new ToolError(`Errors in introspection query result: ${result.errors}`); 23 | } 24 | 25 | fs.writeFileSync(outputPath, JSON.stringify(result, null, 2)); 26 | } 27 | -------------------------------------------------------------------------------- /src/compiler/visitors/generateOperationId.ts: -------------------------------------------------------------------------------- 1 | import { Operation, Fragment } from '../'; 2 | import { collectFragmentsReferenced } from './collectFragmentsReferenced'; 3 | import { createHash } from 'crypto'; 4 | 5 | export function generateOperationId( 6 | operation: Operation, 7 | fragments: { [fragmentName: string]: Fragment }, 8 | fragmentsReferenced?: Iterable 9 | ) { 10 | if (!fragmentsReferenced) { 11 | fragmentsReferenced = collectFragmentsReferenced(operation.selectionSet, fragments); 12 | } 13 | 14 | const sourceWithFragments = [ 15 | operation.source, 16 | ...Array.from(fragmentsReferenced).map(fragmentName => { 17 | const fragment = fragments[fragmentName]; 18 | if (!fragment) { 19 | throw new Error(`Cannot find fragment "${fragmentName}"`); 20 | } 21 | return fragment.source; 22 | }) 23 | ].join('\n'); 24 | 25 | const hash = createHash('sha256'); 26 | hash.update(sourceWithFragments); 27 | const operationId = hash.digest('hex'); 28 | 29 | return { operationId, sourceWithFragments }; 30 | } 31 | -------------------------------------------------------------------------------- /src/compiler/visitors/collectFragmentsReferenced.ts: -------------------------------------------------------------------------------- 1 | import { SelectionSet, Fragment } from '../'; 2 | 3 | export function collectFragmentsReferenced( 4 | selectionSet: SelectionSet, 5 | fragments: { [fragmentName: string]: Fragment }, 6 | fragmentsReferenced: Set = new Set() 7 | ): Set { 8 | for (const selection of selectionSet.selections) { 9 | switch (selection.kind) { 10 | case 'FragmentSpread': 11 | fragmentsReferenced.add(selection.fragmentName); 12 | 13 | const fragment = fragments[selection.fragmentName]; 14 | if (!fragment) { 15 | throw new Error(`Cannot find fragment "${selection.fragmentName}"`); 16 | } 17 | 18 | collectFragmentsReferenced(fragment.selectionSet, fragments, fragmentsReferenced); 19 | break; 20 | case 'Field': 21 | case 'TypeCondition': 22 | case 'BooleanCondition': 23 | if (selection.selectionSet) { 24 | collectFragmentsReferenced(selection.selectionSet, fragments, fragmentsReferenced); 25 | } 26 | break; 27 | } 28 | } 29 | 30 | return fragmentsReferenced; 31 | } 32 | -------------------------------------------------------------------------------- /src/utilities/printing.ts: -------------------------------------------------------------------------------- 1 | // Code generation helper functions copied from graphql-js (https://github.com/graphql/graphql-js) 2 | 3 | /** 4 | * Given maybeArray, print an empty string if it is null or empty, otherwise 5 | * print all items together separated by separator if provided 6 | */ 7 | export function join(maybeArray?: any[], separator?: string) { 8 | return maybeArray ? maybeArray.filter(x => x).join(separator || '') : ''; 9 | } 10 | 11 | /** 12 | * Given array, print each item on its own line, wrapped in an 13 | * indented "{ }" block. 14 | */ 15 | export function block(array: any[]) { 16 | return array && array.length !== 0 ? 17 | indent('{\n' + join(array, '\n')) + '\n}' : 18 | '{}'; 19 | } 20 | 21 | /** 22 | * If maybeString is not null or empty, then wrap with start and end, otherwise 23 | * print an empty string. 24 | */ 25 | export function wrap(start: string, maybeString?: string, end?: string) { 26 | return maybeString ? 27 | start + maybeString + (end || '') : 28 | ''; 29 | } 30 | 31 | export function indent(maybeString?: string) { 32 | return maybeString && maybeString.replace(/\n/g, '\n '); 33 | } 34 | -------------------------------------------------------------------------------- /test/validation.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { loadSchema, loadAndMergeQueryDocuments } from '../src/loading'; 4 | 5 | import { validateQueryDocument } from '../src/validation'; 6 | 7 | const schema = loadSchema(require.resolve('./fixtures/starwars/schema.json')); 8 | 9 | describe('Validation', () => { 10 | function loadQueryDocument(filename: string) { 11 | return loadAndMergeQueryDocuments([ 12 | path.join(__dirname, './fixtures/starwars', filename), 13 | ]); 14 | } 15 | 16 | test(`should throw an error for AnonymousQuery.graphql`, () => { 17 | const document = loadQueryDocument('AnonymousQuery.graphql'); 18 | 19 | expect( 20 | () => validateQueryDocument(schema, document) 21 | ).toThrow( 22 | 'Validation of GraphQL query document failed' 23 | ); 24 | }); 25 | 26 | test(`should throw an error for TypenameAlias.graphql`, () => { 27 | const document = loadQueryDocument('TypenameAlias.graphql'); 28 | 29 | expect( 30 | () => validateQueryDocument(schema, document) 31 | ).toThrow( 32 | 'Validation of GraphQL query document failed' 33 | ); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Meteor Development Group, Inc. 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 | -------------------------------------------------------------------------------- /test/fixtures/misc/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: Query 3 | } 4 | 5 | scalar Date 6 | 7 | type doubleHump { 8 | humpOne: String 9 | humpTwo: String 10 | } 11 | 12 | type OddType { 13 | date: Date 14 | camel: doubleHump 15 | } 16 | 17 | # This is a type to test comments 18 | type CommentTest { 19 | # This is a single-line comment 20 | singleLine: String 21 | # This is a multi-line 22 | # comment. 23 | multiLine: String 24 | enumCommentTest: EnumCommentTestCase 25 | } 26 | 27 | enum EnumCommentTestCase { 28 | # This is a single-line comment 29 | first 30 | # This is a multi-line 31 | # comment. 32 | second 33 | } 34 | 35 | interface InterfaceTestCase { 36 | prop: String! 37 | } 38 | 39 | type ImplA implements InterfaceTestCase { 40 | prop: String! 41 | propA: String! 42 | } 43 | 44 | type ImplB implements InterfaceTestCase { 45 | prop: String! 46 | propB: Int 47 | } 48 | 49 | type PartialA { 50 | prop: String! 51 | } 52 | 53 | type PartialB { 54 | prop: String! 55 | } 56 | 57 | union UnionTestCase = PartialA | PartialB 58 | 59 | type Query { 60 | misc: OddType 61 | commentTest: CommentTest 62 | interfaceTest: InterfaceTestCase 63 | unionTest: UnionTestCase 64 | scalarTest: Boolean! 65 | } 66 | -------------------------------------------------------------------------------- /src/scala/values.js: -------------------------------------------------------------------------------- 1 | import { 2 | join, 3 | wrap, 4 | } from '../utilities/printing'; 5 | 6 | export function escapedString(string) { 7 | return string.replace(/"/g, '\\"'); 8 | } 9 | 10 | export function multilineString(context, string) { 11 | const lines = string.split('\n'); 12 | lines.forEach((line, index) => { 13 | const isLastLine = index != lines.length - 1; 14 | context.printOnNewline(`"${escapedString(line)}"` + (isLastLine ? ' +' : '')); 15 | }); 16 | } 17 | 18 | export function dictionaryLiteralForFieldArguments(args) { 19 | function expressionFromValue(value) { 20 | if (value.kind === 'Variable') { 21 | return `Variable("${value.variableName}")`; 22 | } else if (Array.isArray(value)) { 23 | return wrap('[', join(value.map(expressionFromValue), ', '), ']'); 24 | } else if (typeof value === 'object') { 25 | return wrap('[', join(Object.entries(value).map(([key, value]) => { 26 | return `"${key}": ${expressionFromValue(value)}`; 27 | }), ', ') || ':', ']'); 28 | } else { 29 | return JSON.stringify(value); 30 | } 31 | } 32 | 33 | return wrap('[', join(args.map(arg => { 34 | return `"${arg.name}": ${expressionFromValue(arg.value)}`; 35 | }), ', ') || ':', ']'); 36 | } 37 | -------------------------------------------------------------------------------- /src/flow/types.js: -------------------------------------------------------------------------------- 1 | import { 2 | join, 3 | block, 4 | wrap, 5 | indent 6 | } from '../utilities/printing'; 7 | 8 | import { camelCase } from 'change-case'; 9 | 10 | import { 11 | GraphQLString, 12 | GraphQLInt, 13 | GraphQLFloat, 14 | GraphQLBoolean, 15 | GraphQLID, 16 | GraphQLList, 17 | GraphQLNonNull, 18 | GraphQLScalarType, 19 | GraphQLEnumType 20 | } from 'graphql'; 21 | 22 | const builtInScalarMap = { 23 | [GraphQLString.name]: 'string', 24 | [GraphQLInt.name]: 'number', 25 | [GraphQLFloat.name]: 'number', 26 | [GraphQLBoolean.name]: 'boolean', 27 | [GraphQLID.name]: 'string', 28 | } 29 | 30 | export function typeNameFromGraphQLType(context, type, bareTypeName, nullable = true) { 31 | if (type instanceof GraphQLNonNull) { 32 | return typeNameFromGraphQLType(context, type.ofType, bareTypeName, false) 33 | } 34 | 35 | let typeName; 36 | if (type instanceof GraphQLList) { 37 | typeName = `Array< ${typeNameFromGraphQLType(context, type.ofType, bareTypeName)} >`; 38 | } else if (type instanceof GraphQLScalarType) { 39 | typeName = builtInScalarMap[type.name] || (context.passthroughCustomScalars ? context.customScalarsPrefix + type.name : 'any'); 40 | } else { 41 | typeName = bareTypeName || type.name; 42 | } 43 | 44 | return nullable ? '?' + typeName : typeName; 45 | } 46 | -------------------------------------------------------------------------------- /src/typescript/types.ts: -------------------------------------------------------------------------------- 1 | import { LegacyCompilerContext } from '../compiler/legacyIR'; 2 | 3 | import { 4 | GraphQLString, 5 | GraphQLInt, 6 | GraphQLFloat, 7 | GraphQLBoolean, 8 | GraphQLID, 9 | GraphQLList, 10 | GraphQLNonNull, 11 | GraphQLScalarType, 12 | GraphQLType 13 | } from 'graphql'; 14 | 15 | const builtInScalarMap = { 16 | [GraphQLString.name]: 'string', 17 | [GraphQLInt.name]: 'number', 18 | [GraphQLFloat.name]: 'number', 19 | [GraphQLBoolean.name]: 'boolean', 20 | [GraphQLID.name]: 'string', 21 | } 22 | 23 | export function typeNameFromGraphQLType(context: LegacyCompilerContext, type: GraphQLType, bareTypeName?: string | null, nullable = true): string { 24 | if (type instanceof GraphQLNonNull) { 25 | return typeNameFromGraphQLType(context, type.ofType, bareTypeName, false) 26 | } 27 | 28 | let typeName; 29 | if (type instanceof GraphQLList) { 30 | typeName = `Array< ${typeNameFromGraphQLType(context, type.ofType, bareTypeName, true)} >`; 31 | } else if (type instanceof GraphQLScalarType) { 32 | typeName = builtInScalarMap[type.name] || (context.options.passthroughCustomScalars ? context.options.customScalarsPrefix + type.name : builtInScalarMap[GraphQLString.name]); 33 | } else { 34 | typeName = bareTypeName || type.name; 35 | } 36 | 37 | return nullable ? typeName + ' | null' : typeName; 38 | } 39 | -------------------------------------------------------------------------------- /src/downloadSchema.ts: -------------------------------------------------------------------------------- 1 | // Based on https://facebook.github.io/relay/docs/guides-babel-plugin.html#using-other-graphql-implementations 2 | 3 | import fetch from 'node-fetch'; 4 | import * as fs from 'fs'; 5 | import * as https from 'https'; 6 | 7 | import { 8 | introspectionQuery, 9 | } from 'graphql/utilities'; 10 | 11 | import { ToolError } from './errors' 12 | 13 | const defaultHeaders = { 14 | 'Accept': 'application/json', 15 | 'Content-Type': 'application/json' 16 | }; 17 | 18 | export default async function downloadSchema(url: string, outputPath: string, additionalHeaders: { [name: string]: string }, insecure: boolean, method: string) { 19 | const headers: { [index: string]: string } = Object.assign(defaultHeaders, additionalHeaders); 20 | const agent = insecure ? new https.Agent({ rejectUnauthorized: false }) : undefined; 21 | 22 | let result; 23 | try { 24 | const response = await fetch(url, { 25 | method: method, 26 | headers: headers, 27 | body: JSON.stringify({ 'query': introspectionQuery }), 28 | agent, 29 | }); 30 | 31 | result = await response.json(); 32 | } catch (error) { 33 | throw new ToolError(`Error while fetching introspection query result: ${error.message}`); 34 | } 35 | 36 | if (result.errors) { 37 | throw new ToolError(`Errors in introspection query result: ${result.errors}`); 38 | } 39 | 40 | const schemaData = result; 41 | if (!schemaData.data) { 42 | throw new ToolError(`No introspection query result data found, server responded with: ${JSON.stringify(result)}`); 43 | } 44 | 45 | fs.writeFileSync(outputPath, JSON.stringify(schemaData, null, 2)); 46 | } 47 | -------------------------------------------------------------------------------- /src/validation.ts: -------------------------------------------------------------------------------- 1 | import { 2 | validate, 3 | specifiedRules, 4 | NoUnusedFragmentsRule, 5 | GraphQLError, 6 | FieldNode, 7 | ValidationContext, 8 | GraphQLSchema, 9 | DocumentNode, 10 | OperationDefinitionNode 11 | } from 'graphql'; 12 | 13 | import { ToolError, logError } from './errors'; 14 | 15 | export function validateQueryDocument(schema: GraphQLSchema, document: DocumentNode) { 16 | const specifiedRulesToBeRemoved = [NoUnusedFragmentsRule]; 17 | 18 | const rules = [ 19 | NoAnonymousQueries, 20 | NoTypenameAlias, 21 | ...specifiedRules.filter(rule => !specifiedRulesToBeRemoved.includes(rule)) 22 | ]; 23 | 24 | const validationErrors = validate(schema, document, rules); 25 | if (validationErrors && validationErrors.length > 0) { 26 | for (const error of validationErrors) { 27 | logError(error); 28 | } 29 | throw new ToolError('Validation of GraphQL query document failed'); 30 | } 31 | } 32 | 33 | export function NoAnonymousQueries(context: ValidationContext) { 34 | return { 35 | OperationDefinition(node: OperationDefinitionNode) { 36 | if (!node.name) { 37 | context.reportError(new GraphQLError('Apollo does not support anonymous operations', [node])); 38 | } 39 | return false; 40 | } 41 | }; 42 | } 43 | 44 | export function NoTypenameAlias(context: ValidationContext) { 45 | return { 46 | Field(node: FieldNode) { 47 | const aliasName = node.alias && node.alias.value; 48 | if (aliasName == '__typename') { 49 | context.reportError( 50 | new GraphQLError( 51 | 'Apollo needs to be able to insert __typename when needed, please do not use it as an alias', 52 | [node] 53 | ) 54 | ); 55 | } 56 | } 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /src/scala/types.js: -------------------------------------------------------------------------------- 1 | import { 2 | join, 3 | block, 4 | wrap, 5 | indent 6 | } from '../utilities/printing'; 7 | 8 | import { camelCase } from 'change-case'; 9 | 10 | import { 11 | GraphQLString, 12 | GraphQLInt, 13 | GraphQLFloat, 14 | GraphQLBoolean, 15 | GraphQLID, 16 | GraphQLList, 17 | GraphQLNonNull, 18 | GraphQLScalarType, 19 | GraphQLEnumType, 20 | isCompositeType, 21 | isAbstractType 22 | } from 'graphql'; 23 | 24 | const builtInScalarMap = { 25 | [GraphQLString.name]: 'String', 26 | [GraphQLInt.name]: 'Int', 27 | [GraphQLFloat.name]: 'Double', 28 | [GraphQLBoolean.name]: 'Boolean', 29 | [GraphQLID.name]: 'String', 30 | } 31 | 32 | export function possibleTypesForType(context, type) { 33 | if (isAbstractType(type)) { 34 | return context.schema.getPossibleTypes(type); 35 | } else { 36 | return [type]; 37 | } 38 | } 39 | 40 | export function typeNameFromGraphQLType(context, type, bareTypeName, isOptional) { 41 | if (type instanceof GraphQLNonNull) { 42 | return typeNameFromGraphQLType(context, type.ofType, bareTypeName, isOptional || false) 43 | } else if (isOptional === undefined) { 44 | isOptional = true; 45 | } 46 | 47 | let typeName; 48 | if (type instanceof GraphQLList) { 49 | typeName = 'Seq[' + typeNameFromGraphQLType(context, type.ofType, bareTypeName) + ']'; 50 | } else if (type instanceof GraphQLScalarType) { 51 | typeName = typeNameForScalarType(context, type); 52 | } else if (type instanceof GraphQLEnumType) { 53 | typeName = "String"; 54 | } else { 55 | typeName = bareTypeName || type.name; 56 | } 57 | 58 | return isOptional ? `Option[${typeName}]` : typeName; 59 | } 60 | 61 | function typeNameForScalarType(context, type) { 62 | return builtInScalarMap[type.name] || (context.passthroughCustomScalars ? context.customScalarsPrefix + type.name: GraphQLString) 63 | } 64 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from 'graphql'; 2 | import * as path from 'path'; 3 | 4 | // ToolError is used for errors that are part of the expected flow 5 | // and for which a stack trace should not be printed 6 | 7 | export class ToolError extends Error { 8 | name: string = 'ToolError'; 9 | 10 | constructor(message: string) { 11 | super(message); 12 | this.message = message; 13 | } 14 | } 15 | 16 | const isRunningFromXcodeScript = process.env.XCODE_VERSION_ACTUAL; 17 | 18 | export function logError(error: Error) { 19 | if (error instanceof ToolError) { 20 | logErrorMessage(error.message); 21 | } else if (error instanceof GraphQLError) { 22 | const fileName = error.source && error.source.name; 23 | if (error.locations) { 24 | for (const location of error.locations) { 25 | logErrorMessage(error.message, fileName, location.line); 26 | } 27 | } else { 28 | logErrorMessage(error.message, fileName); 29 | } 30 | } else { 31 | console.log(error.stack); 32 | } 33 | } 34 | 35 | export function logErrorMessage(message: string, fileName?: string, lineNumber?: number) { 36 | if (isRunningFromXcodeScript) { 37 | if (fileName && lineNumber) { 38 | // Prefixing error output with file name, line and 'error: ', 39 | // so Xcode will associate it with the right file and display the error inline 40 | console.log(`${fileName}:${lineNumber}: error: ${message}`); 41 | } else { 42 | // Prefixing error output with 'error: ', so Xcode will display it as an error 43 | console.log(`error: ${message}`); 44 | } 45 | } else { 46 | if (fileName) { 47 | const truncatedFileName = '/' + fileName.split(path.sep).slice(-4).join(path.sep); 48 | console.log(`...${truncatedFileName}: ${message}`); 49 | } else { 50 | console.log(`error: ${message}`); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/flow-modern/helpers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLBoolean, 3 | GraphQLFloat, 4 | GraphQLInt, 5 | GraphQLID, 6 | GraphQLList, 7 | GraphQLNonNull, 8 | GraphQLScalarType, 9 | GraphQLString, 10 | GraphQLType, 11 | } from 'graphql' 12 | 13 | import * as t from 'babel-types'; 14 | 15 | import { CompilerOptions } from '../compiler'; 16 | 17 | const builtInScalarMap = { 18 | [GraphQLString.name]: t.stringTypeAnnotation(), 19 | [GraphQLInt.name]: t.numberTypeAnnotation(), 20 | [GraphQLFloat.name]: t.numberTypeAnnotation(), 21 | [GraphQLBoolean.name]: t.booleanTypeAnnotation(), 22 | [GraphQLID.name]: t.stringTypeAnnotation(), 23 | } 24 | 25 | export function createTypeAnnotationFromGraphQLTypeFunction( 26 | compilerOptions: CompilerOptions 27 | ): Function { 28 | return function typeAnnotationFromGraphQLType(type: GraphQLType, { 29 | nullable 30 | } = { 31 | nullable: true 32 | }): t.FlowTypeAnnotation { 33 | if (type instanceof GraphQLNonNull) { 34 | return typeAnnotationFromGraphQLType( 35 | type.ofType, 36 | { nullable: false } 37 | ); 38 | } 39 | 40 | if (type instanceof GraphQLList) { 41 | const typeAnnotation = t.arrayTypeAnnotation( 42 | typeAnnotationFromGraphQLType(type.ofType) 43 | ); 44 | 45 | if (nullable) { 46 | return t.nullableTypeAnnotation(typeAnnotation); 47 | } else { 48 | return typeAnnotation; 49 | } 50 | } 51 | 52 | let typeAnnotation; 53 | if (type instanceof GraphQLScalarType) { 54 | const builtIn = builtInScalarMap[type.name] 55 | if (builtIn) { 56 | typeAnnotation = builtIn; 57 | } else { 58 | if (compilerOptions.passthroughCustomScalars) { 59 | typeAnnotation = t.anyTypeAnnotation(); 60 | } else { 61 | typeAnnotation = t.genericTypeAnnotation( 62 | t.identifier(type.name) 63 | ); 64 | } 65 | } 66 | } else { 67 | typeAnnotation = t.genericTypeAnnotation( 68 | t.identifier(type.name) 69 | ); 70 | } 71 | 72 | if (nullable) { 73 | return t.nullableTypeAnnotation(typeAnnotation); 74 | } else { 75 | return typeAnnotation; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/scala/language.js: -------------------------------------------------------------------------------- 1 | import { 2 | join, 3 | wrap, 4 | } from '../utilities/printing'; 5 | 6 | export function comment(generator, comment) { 7 | const split = comment ? comment.split('\n') : []; 8 | if (split.length > 0) { 9 | generator.printOnNewline('/**') 10 | split.forEach(line => { 11 | generator.printOnNewline(` * ${line.trim()}`); 12 | }); 13 | 14 | generator.printOnNewline(' */'); 15 | } 16 | } 17 | 18 | export function packageDeclaration(generator, pkg) { 19 | generator.printNewlineIfNeeded(); 20 | generator.printOnNewline(`package ${pkg}`); 21 | generator.popScope(); 22 | } 23 | 24 | export function objectDeclaration(generator, { objectName, superclass, properties }, closure) { 25 | generator.printNewlineIfNeeded(); 26 | generator.printOnNewline(`object ${objectName}` + (superclass ? ` extends ${superclass}` : '')); 27 | generator.pushScope({ typeName: objectName }); 28 | generator.withinBlock(closure); 29 | generator.popScope(); 30 | } 31 | 32 | export function caseClassDeclaration(generator, { caseClassName, description, superclass, params }, closure) { 33 | generator.printNewlineIfNeeded(); 34 | comment(generator, description); 35 | generator.printOnNewline(`case class ${caseClassName}(${(params || []).map(v => v.name + ": " + v.type).join(', ')})` + (superclass ? ` extends ${superclass}` : '')); 36 | generator.pushScope({ typeName: caseClassName }); 37 | generator.withinBlock(closure); 38 | generator.popScope(); 39 | } 40 | 41 | export function propertyDeclaration(generator, { propertyName, typeName, description}, closure) { 42 | comment(generator, description); 43 | generator.printOnNewline(`val ${propertyName}: ${typeName} =`); 44 | generator.withinBlock(closure); 45 | } 46 | 47 | export function propertyDeclarations(generator, declarations) { 48 | declarations.forEach(o => { 49 | propertyDeclaration(generator, o); 50 | }); 51 | } 52 | 53 | const reservedKeywords = new Set( 54 | 'case', 'catch', 'class', 'def', 'do', 'else', 55 | 'extends', 'false', 'final', 'for', 'if', 'match', 56 | 'new', 'null', 'throw', 'trait', 'true', 'try', 'until', 57 | 'val', 'var', 'while', 'with' 58 | ); 59 | 60 | export function escapeIdentifierIfNeeded(identifier) { 61 | if (reservedKeywords.has(identifier)) { 62 | return '`' + identifier + '`'; 63 | } else { 64 | return identifier; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/serializeToJSON.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isType, 3 | GraphQLType, 4 | GraphQLScalarType, 5 | GraphQLEnumType, 6 | GraphQLInputObjectType, 7 | } from 'graphql'; 8 | 9 | import { LegacyCompilerContext } from './compiler/legacyIR'; 10 | 11 | export default function serializeToJSON(context: LegacyCompilerContext) { 12 | return serializeAST({ 13 | operations: Object.values(context.operations), 14 | fragments: Object.values(context.fragments), 15 | typesUsed: context.typesUsed.map(serializeType), 16 | }, '\t'); 17 | } 18 | 19 | export function serializeAST(ast: any, space?: string) { 20 | return JSON.stringify(ast, function(_, value) { 21 | if (isType(value)) { 22 | return String(value); 23 | } else { 24 | return value; 25 | } 26 | }, space); 27 | } 28 | 29 | function serializeType(type: GraphQLType) { 30 | if (type instanceof GraphQLEnumType) { 31 | return serializeEnumType(type); 32 | } else if (type instanceof GraphQLInputObjectType) { 33 | return serializeInputObjectType(type); 34 | } else if (type instanceof GraphQLScalarType) { 35 | return serializeScalarType(type); 36 | } else { 37 | throw new Error(`Unexpected GraphQL type: ${type}`); 38 | } 39 | } 40 | 41 | function serializeEnumType(type: GraphQLEnumType) { 42 | const { name, description } = type; 43 | const values = type.getValues(); 44 | 45 | return { 46 | kind: 'EnumType', 47 | name, 48 | description, 49 | values: values.map(value => ( 50 | { 51 | name: value.name, 52 | description: value.description, 53 | isDeprecated: value.isDeprecated, 54 | deprecationReason: value.deprecationReason 55 | } 56 | )) 57 | } 58 | } 59 | 60 | function serializeInputObjectType(type: GraphQLInputObjectType) { 61 | const { name, description } = type; 62 | const fields = Object.values(type.getFields()); 63 | 64 | return { 65 | kind: 'InputObjectType', 66 | name, 67 | description, 68 | fields: fields.map(field => ({ 69 | name: field.name, 70 | type: String(field.type), 71 | description: field.description, 72 | defaultValue: field.defaultValue 73 | })) 74 | } 75 | } 76 | 77 | function serializeScalarType(type: GraphQLScalarType) { 78 | const { name, description } = type; 79 | 80 | return { 81 | kind: 'ScalarType', 82 | name, 83 | description 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /test/valueFromValueNode.ts: -------------------------------------------------------------------------------- 1 | import { parseValue } from 'graphql'; 2 | 3 | import { valueFromValueNode } from '../src/utilities/graphql'; 4 | 5 | describe('#valueFromValueNode', () => { 6 | test(`should return a number for an IntValue`, () => { 7 | const valueNode = parseValue('1'); 8 | const value = valueFromValueNode(valueNode); 9 | 10 | expect(value).toBe(1); 11 | }); 12 | 13 | test(`should return a number for a FloatValue`, () => { 14 | const valueNode = parseValue('1.0'); 15 | const value = valueFromValueNode(valueNode); 16 | 17 | expect(value).toBe(1.0); 18 | }); 19 | 20 | test(`should return a boolean for a BooleanValue`, () => { 21 | const valueNode = parseValue('true'); 22 | const value = valueFromValueNode(valueNode); 23 | 24 | expect(value).toBe(true); 25 | }); 26 | 27 | test(`should return null for a NullValue`, () => { 28 | const valueNode = parseValue('null'); 29 | const value = valueFromValueNode(valueNode); 30 | 31 | expect(value).toBe(null); 32 | }); 33 | 34 | test(`should return a string for a StringValue`, () => { 35 | const valueNode = parseValue('"foo"'); 36 | const value = valueFromValueNode(valueNode); 37 | 38 | expect(value).toBe('foo'); 39 | }); 40 | 41 | test(`should return a string for an EnumValue`, () => { 42 | const valueNode = parseValue('JEDI'); 43 | const value = valueFromValueNode(valueNode); 44 | 45 | expect(value).toBe('JEDI'); 46 | }); 47 | 48 | test(`should return an object for a Variable`, () => { 49 | const valueNode = parseValue('$something'); 50 | const value = valueFromValueNode(valueNode); 51 | 52 | expect(value).toEqual({ kind: 'Variable', variableName: 'something' }); 53 | }); 54 | 55 | test(`should return an array for a ListValue`, () => { 56 | const valueNode = parseValue('[ "foo", 1, JEDI, $something ]'); 57 | const value = valueFromValueNode(valueNode); 58 | 59 | expect(value).toEqual(['foo', 1, 'JEDI', { kind: 'Variable', variableName: 'something' }]); 60 | }); 61 | 62 | test(`should return an object for an ObjectValue`, () => { 63 | const valueNode = parseValue('{ foo: "foo", bar: 1, bla: JEDI, baz: $something }'); 64 | const value = valueFromValueNode(valueNode); 65 | 66 | expect(value).toEqual({ 67 | foo: 'foo', 68 | bar: 1, 69 | bla: 'JEDI', 70 | baz: { kind: 'Variable', variableName: 'something' } 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/scala/language.js: -------------------------------------------------------------------------------- 1 | import { stripIndent } from 'common-tags'; 2 | 3 | import CodeGenerator from '../../src/utilities/CodeGenerator'; 4 | 5 | import { 6 | objectDeclaration, 7 | caseClassDeclaration, 8 | propertyDeclaration, 9 | } from '../../src/scala/language'; 10 | 11 | describe('Scala code generation: Basic language constructs', function() { 12 | let generator; 13 | 14 | beforeEach(function() { 15 | generator = new CodeGenerator(); 16 | }); 17 | 18 | test(`should generate a object declaration`, function() { 19 | objectDeclaration(generator, { objectName: 'Hero' }, () => { 20 | propertyDeclaration(generator, { propertyName: 'name', typeName: 'String' }, () => {}); 21 | propertyDeclaration(generator, { propertyName: 'age', typeName: 'Int' }, () => {}); 22 | }); 23 | 24 | expect(generator.output).toBe(stripIndent` 25 | object Hero { 26 | val name: String = { 27 | } 28 | val age: Int = { 29 | } 30 | } 31 | `); 32 | }); 33 | 34 | test(`should generate a case class declaration`, function() { 35 | caseClassDeclaration(generator, { caseClassName: 'Hero', params: [{name: 'name', type: 'String'}, {name: 'age', type: 'Int'}] }, () => {}); 36 | 37 | expect(generator.output).toBe(stripIndent` 38 | case class Hero(name: String, age: Int) { 39 | } 40 | `); 41 | }); 42 | 43 | test(`should generate nested case class declarations`, function() { 44 | caseClassDeclaration(generator, { caseClassName: 'Hero', params: [{name: 'name', type: 'String'}, {name: 'age', type: 'Int'}] }, () => { 45 | caseClassDeclaration(generator, { caseClassName: 'Friend', params: [{name: 'name', type: 'String'}] }, () => {}); 46 | }); 47 | 48 | expect(generator.output).toBe(stripIndent` 49 | case class Hero(name: String, age: Int) { 50 | case class Friend(name: String) { 51 | } 52 | } 53 | `); 54 | }); 55 | 56 | test(`should handle multi-line descriptions`, () => { 57 | caseClassDeclaration(generator, { caseClassName: 'Hero', description: 'A hero' }, () => { 58 | propertyDeclaration(generator, { propertyName: 'name', typeName: 'String', description: `A multiline comment \n on the hero's name.` }, () => {}); 59 | propertyDeclaration(generator, { propertyName: 'age', typeName: 'String', description: `A multiline comment \n on the hero's age.` }, () => {}); 60 | }); 61 | 62 | expect(generator.output).toMatchSnapshot(); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/loading.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | 3 | import { 4 | buildClientSchema, 5 | Source, 6 | concatAST, 7 | parse, 8 | DocumentNode, 9 | GraphQLSchema 10 | } from 'graphql'; 11 | 12 | import { 13 | getGraphQLProjectConfig, 14 | ConfigNotFoundError 15 | } from 'graphql-config' 16 | 17 | import { ToolError } from './errors' 18 | 19 | export function loadSchema(schemaPath: string): GraphQLSchema { 20 | if (!fs.existsSync(schemaPath)) { 21 | throw new ToolError(`Cannot find GraphQL schema file: ${schemaPath}`); 22 | } 23 | const schemaData = require(schemaPath); 24 | 25 | if (!schemaData.data && !schemaData.__schema) { 26 | throw new ToolError('GraphQL schema file should contain a valid GraphQL introspection query result'); 27 | } 28 | return buildClientSchema((schemaData.data) ? schemaData.data : schemaData); 29 | } 30 | 31 | export function loadSchemaFromConfig(projectName: string): GraphQLSchema { 32 | try { 33 | const config = getGraphQLProjectConfig('.', projectName); 34 | return config.getSchema(); 35 | } catch (e) { 36 | if (!(e instanceof ConfigNotFoundError)) { 37 | throw e; 38 | } 39 | } 40 | 41 | const defaultSchemaPath = 'schema.json'; 42 | 43 | if (fs.existsSync(defaultSchemaPath)) { 44 | return loadSchema('schema.json'); 45 | } 46 | 47 | throw new ToolError(`No GraphQL schema specified. There must either be a .graphqlconfig or a ${defaultSchemaPath} file present, or you must use the --schema option.`); 48 | } 49 | 50 | function extractDocumentFromJavascript(content: string, tagName: string = 'gql'): string | null { 51 | const re = new RegExp(tagName + '\\s*`([^`/]*)`', 'g'); 52 | 53 | let match 54 | const matches = [] 55 | 56 | while(match = re.exec(content)) { 57 | const doc = match[1] 58 | .replace(/\${[^}]*}/g, '') 59 | 60 | matches.push(doc) 61 | } 62 | 63 | const doc = matches.join('\n') 64 | return doc.length ? doc : null; 65 | } 66 | 67 | export function loadAndMergeQueryDocuments(inputPaths: string[], tagName: string = 'gql'): DocumentNode { 68 | const sources = inputPaths.map(inputPath => { 69 | const body = fs.readFileSync(inputPath, 'utf8'); 70 | if (!body) { 71 | return null; 72 | } 73 | 74 | if (inputPath.endsWith('.jsx') || inputPath.endsWith('.js') 75 | || inputPath.endsWith('.tsx') || inputPath.endsWith('.ts') 76 | ) { 77 | const doc = extractDocumentFromJavascript(body.toString(), tagName); 78 | return doc ? new Source(doc, inputPath) : null; 79 | } 80 | 81 | return new Source(body, inputPath); 82 | }).filter(source => source); 83 | 84 | return concatAST((sources as Source[]).map(source => parse(source))); 85 | } 86 | -------------------------------------------------------------------------------- /test/test-utils/matchers.ts: -------------------------------------------------------------------------------- 1 | import { collectAndMergeFields } from '../../src/compiler/visitors/collectAndMergeFields'; 2 | 3 | import { SelectionSet } from '../../src/compiler'; 4 | 5 | declare global { 6 | namespace jest { 7 | interface Matchers { 8 | toMatchSelectionSet(possibleTypeNames: string[], expectedResponseKeys: string[]): R; 9 | toContainSelectionSetMatching(possibleTypeNames: string[], expectedResponseKeys: string[]): R; 10 | } 11 | interface MatcherUtils { 12 | equals(a: any, b: any): boolean; 13 | } 14 | } 15 | } 16 | 17 | function toMatchSelectionSet( 18 | this: jest.MatcherUtils, 19 | received: SelectionSet, 20 | possibleTypeNames: string[], 21 | expectedResponseKeys: string[] 22 | ): { message(): string; pass: boolean } { 23 | const actualResponseKeys = collectAndMergeFields(received).map(field => field.responseKey); 24 | 25 | const pass = this.equals(actualResponseKeys, expectedResponseKeys); 26 | 27 | if (pass) { 28 | return { 29 | message: () => 30 | `Expected selection set for ${this.utils.printExpected(possibleTypeNames)}\n` + 31 | `To not match:\n` + 32 | ` ${this.utils.printExpected(expectedResponseKeys)}` + 33 | 'Received:\n' + 34 | ` ${this.utils.printReceived(actualResponseKeys)}`, 35 | pass: true 36 | }; 37 | } else { 38 | return { 39 | message: () => 40 | `Expected selection set for ${this.utils.printExpected(possibleTypeNames)}\n` + 41 | `To match:\n` + 42 | ` ${this.utils.printExpected(expectedResponseKeys)}\n` + 43 | 'Received:\n' + 44 | ` ${this.utils.printReceived(actualResponseKeys)}`, 45 | pass: false 46 | }; 47 | } 48 | } 49 | 50 | function toContainSelectionSetMatching( 51 | this: jest.MatcherUtils, 52 | received: SelectionSet[], 53 | possibleTypeNames: string[], 54 | expectedResponseKeys: string[] 55 | ): { message(): string; pass: boolean } { 56 | const variant = received.find(variant => { 57 | return this.equals(Array.from(variant.possibleTypes).map(type => type.name), possibleTypeNames); 58 | }); 59 | 60 | if (!variant) { 61 | return { 62 | message: () => 63 | `Expected array to contain variant for:\n` + 64 | ` ${this.utils.printExpected(possibleTypeNames)}\n` + 65 | `But only found variants for:\n` + 66 | received 67 | .map( 68 | variant => 69 | ` ${this.utils.printReceived(variant.possibleTypes)} -> ${this.utils.printReceived( 70 | collectAndMergeFields(variant).map(field => field.name) 71 | )}` 72 | ) 73 | .join('\n'), 74 | pass: false 75 | }; 76 | } 77 | 78 | return toMatchSelectionSet.call(this, variant, possibleTypeNames, expectedResponseKeys); 79 | } 80 | 81 | expect.extend({ 82 | toMatchSelectionSet, 83 | toContainSelectionSetMatching 84 | } as any); 85 | -------------------------------------------------------------------------------- /src/swift/s3Wrapper.ts: -------------------------------------------------------------------------------- 1 | // Blank lines at the beginning are intentional 2 | export const s3WrapperCode = ` 3 | 4 | extension S3Object: AWSS3ObjectProtocol { 5 | public func getBucketName() -> String { 6 | return bucket 7 | } 8 | 9 | public func getKeyName() -> String { 10 | return key 11 | } 12 | 13 | public func getRegion() -> String { 14 | return region 15 | } 16 | } 17 | 18 | extension S3ObjectInput: AWSS3ObjectProtocol, AWSS3InputObjectProtocol { 19 | public func getLocalSourceFileURL() -> URL? { 20 | return URL(string: self.localUri) 21 | } 22 | 23 | public func getMimeType() -> String { 24 | return self.mimeType 25 | } 26 | 27 | public func getBucketName() -> String { 28 | return self.bucket 29 | } 30 | 31 | public func getKeyName() -> String { 32 | return self.key 33 | } 34 | 35 | public func getRegion() -> String { 36 | return self.region 37 | } 38 | 39 | } 40 | 41 | import AWSS3 42 | extension AWSS3PreSignedURLBuilder: AWSS3ObjectPresignedURLGenerator { 43 | public func getPresignedURL(s3Object: AWSS3ObjectProtocol) -> URL? { 44 | let request = AWSS3GetPreSignedURLRequest() 45 | request.bucket = s3Object.getBucketName() 46 | request.key = s3Object.getKeyName() 47 | var url : URL? 48 | self.getPreSignedURL(request).continueWith { (task) -> Any? in 49 | url = task.result as URL? 50 | }.waitUntilFinished() 51 | return url 52 | } 53 | } 54 | 55 | extension AWSS3TransferUtility: AWSS3ObjectManager { 56 | 57 | public func download(s3Object: AWSS3ObjectProtocol, toURL: URL, completion: @escaping ((Bool, Error?) -> Void)) { 58 | 59 | let completionBlock: AWSS3TransferUtilityDownloadCompletionHandlerBlock = { task, url, data, error -> Void in 60 | if let _ = error { 61 | completion(false, error) 62 | } else { 63 | completion(true, nil) 64 | } 65 | } 66 | let _ = self.download(to: toURL, bucket: s3Object.getBucketName(), key: s3Object.getKeyName(), expression: nil, completionHandler: completionBlock) 67 | } 68 | 69 | public func upload(s3Object: AWSS3ObjectProtocol & AWSS3InputObjectProtocol, completion: @escaping ((_ success: Bool, _ error: Error?) -> Void)) { 70 | let completionBlock : AWSS3TransferUtilityUploadCompletionHandlerBlock = { task, error -> Void in 71 | if let _ = error { 72 | completion(false, error) 73 | } else { 74 | completion(true, nil) 75 | } 76 | } 77 | let _ = self.uploadFile(s3Object.getLocalSourceFileURL()!, bucket: s3Object.getBucketName(), key: s3Object.getKeyName(), contentType: s3Object.getMimeType(), expression: nil, completionHandler: completionBlock).continueWith { (task) -> Any? in 78 | if let err = task.error { 79 | completion(false, err) 80 | } 81 | return nil 82 | } 83 | } 84 | }`; 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-appsync-codegen", 3 | "version": "0.17.5", 4 | "description": "Generate API code or type annotations based on a GraphQL schema and query documents", 5 | "main": "./lib/index.js", 6 | "bin": "./lib/cli.js", 7 | "scripts": { 8 | "clean": "rm -rf lib", 9 | "compile": "tsc", 10 | "watch": "tsc -w", 11 | "prepublish": "npm run clean && npm run compile", 12 | "test": "./node_modules/.bin/jest", 13 | "test:smoke": "npm install && npm run compile && rm -rf node_modules && npm install --prod && node ./lib/cli.js && echo 'Smoke Test Passed'" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "awslabs/aws-appsync-codegen" 18 | }, 19 | "original-author": "Martijn Walraven ", 20 | "author": "AWS", 21 | "contributors": [ 22 | "Min Bi", 23 | "Rohan Dubal", 24 | "Martijn Walraven " 25 | ], 26 | "license": "MIT", 27 | "engines": { 28 | "node": ">=6.0" 29 | }, 30 | "devDependencies": { 31 | "@types/common-tags": "^1.2.5", 32 | "@types/glob": "^5.0.30", 33 | "@types/graphql": "^0.11.3", 34 | "@types/inflected": "^1.1.29", 35 | "@types/jest": "^20.0.6", 36 | "@types/mkdirp": "^0.5.0", 37 | "@types/node-fetch": "^1.6.7", 38 | "@types/yargs": "^8.0.2", 39 | "ansi-regex": "^3.0.0", 40 | "jest": "^21.2.1", 41 | "jest-matcher-utils": "^21.1.0", 42 | "ts-jest": "^21.0.1", 43 | "typescript": "^2.5.2" 44 | }, 45 | "dependencies": { 46 | "@babel/generator": "^7.0.0-beta.4", 47 | "@babel/types": "^7.0.0-beta.4", 48 | "@types/babel-generator": "^6.25.0", 49 | "@types/babylon": "^6.16.2", 50 | "@types/rimraf": "^2.0.2", 51 | "babel-generator": "^6.26.1", 52 | "babylon": "^6.18.0", 53 | "change-case": "^3.0.1", 54 | "common-tags": "^1.4.0", 55 | "core-js": "^2.5.0", 56 | "glob": "^7.1.2", 57 | "graphql": "^0.11.3", 58 | "graphql-config": "^1.0.5", 59 | "inflected": "^2.0.2", 60 | "jest-cli": "^21.2.1", 61 | "mkdirp": "^0.5.1", 62 | "node-fetch": "^1.7.2", 63 | "npm": "^6.2.0", 64 | "rimraf": "^2.6.2", 65 | "source-map-support": "^0.4.15", 66 | "yargs": "^8.0.2" 67 | }, 68 | "jest": { 69 | "testEnvironment": "node", 70 | "setupFiles": [ 71 | "/src/polyfills.js" 72 | ], 73 | "setupTestFrameworkScriptFile": "/test/test-utils/matchers.ts", 74 | "testMatch": [ 75 | "**/test/**/*.(js|ts)", 76 | "**/test/*.(js|ts)", 77 | "**/__tests__/*.(js|ts)" 78 | ], 79 | "testPathIgnorePatterns": [ 80 | "/node_modules/", 81 | "/lib/", 82 | "/test/fixtures/", 83 | "/test/test-utils" 84 | ], 85 | "transform": { 86 | "^.+\\.(ts|js)x?$": "/node_modules/ts-jest/preprocessor.js" 87 | }, 88 | "moduleFileExtensions": [ 89 | "ts", 90 | "js" 91 | ] 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/scala/naming.js: -------------------------------------------------------------------------------- 1 | import { camelCase, pascalCase } from 'change-case'; 2 | import * as Inflector from 'inflected'; 3 | 4 | import { 5 | join 6 | } from '../utilities/printing'; 7 | 8 | import { 9 | escapeIdentifierIfNeeded 10 | } from './language'; 11 | 12 | import { 13 | typeNameFromGraphQLType 14 | } from './types'; 15 | 16 | import { 17 | GraphQLError, 18 | GraphQLList, 19 | GraphQLNonNull, 20 | getNamedType, 21 | isCompositeType, 22 | } from 'graphql'; 23 | 24 | export function enumCaseName(name) { 25 | return camelCase(name); 26 | } 27 | 28 | export function operationClassName(name) { 29 | return pascalCase(name); 30 | } 31 | 32 | export function caseClassNameForPropertyName(propertyName) { 33 | return pascalCase(Inflector.singularize(propertyName)); 34 | } 35 | 36 | export function caseClassNameForFragmentName(fragmentName) { 37 | return pascalCase(fragmentName); 38 | } 39 | 40 | export function caseClassNameForInlineFragment(inlineFragment) { 41 | return 'As' + pascalCase(String(inlineFragment.typeCondition)); 42 | } 43 | 44 | export function propertyFromField(context, field, namespace) { 45 | const name = field.name || field.responseName; 46 | const unescapedPropertyName = isMetaFieldName(name) ? name : camelCase(name) 47 | const propertyName = escapeIdentifierIfNeeded(unescapedPropertyName); 48 | 49 | const type = field.type; 50 | const isList = type instanceof GraphQLList || type.ofType instanceof GraphQLList 51 | const isOptional = field.isConditional || !(type instanceof GraphQLNonNull); 52 | const bareType = getNamedType(type); 53 | 54 | if (isCompositeType(bareType)) { 55 | const bareTypeName = join([ 56 | namespace, 57 | escapeIdentifierIfNeeded(pascalCase(Inflector.singularize(name))) 58 | ], '.'); 59 | const typeName = typeNameFromGraphQLType(context, type, bareTypeName, isOptional); 60 | return { ...field, propertyName, typeName, bareTypeName, isOptional, isList, isComposite: true }; 61 | } else { 62 | const typeName = typeNameFromGraphQLType(context, type, undefined, isOptional); 63 | return { ...field, propertyName, typeName, isOptional, isList, isComposite: false }; 64 | } 65 | } 66 | 67 | export function propertyFromInlineFragment(context, inlineFragment) { 68 | const structName = caseClassNameForInlineFragment(inlineFragment); 69 | const propertyName = camelCase(structName); 70 | const typeName = structName + '?' 71 | return { propertyName, typeName, structName, isComposite: true, ...inlineFragment }; 72 | } 73 | 74 | export function propertyFromFragmentSpread(context, fragmentSpread) { 75 | const fragmentName = fragmentSpread; 76 | const fragment = context.fragments[fragmentName]; 77 | if (!fragment) { 78 | throw new GraphQLError(`Cannot find fragment "${fragmentName}"`); 79 | } 80 | const propertyName = camelCase(fragmentName); 81 | const typeName = caseClassNameForFragmentName(fragmentName); 82 | return { propertyName, typeName, fragment, isComposite: true }; 83 | } 84 | 85 | function isMetaFieldName(name) { 86 | return name.startsWith("__"); 87 | } -------------------------------------------------------------------------------- /src/flow-modern/printer.ts: -------------------------------------------------------------------------------- 1 | import * as t from '@babel/types'; 2 | import generate from '@babel/generator'; 3 | 4 | import { stripIndent } from 'common-tags'; 5 | 6 | type Printable = t.Node | string; 7 | 8 | export default class Printer { 9 | private printQueue: Printable[] = [] 10 | 11 | public print(): string { 12 | return this.printQueue 13 | .reduce( 14 | (document: string, printable) => { 15 | if (typeof printable === 'string') { 16 | return document + printable; 17 | } else { 18 | const documentPart = generate(printable).code; 19 | return document + this.fixCommas(documentPart); 20 | } 21 | }, 22 | '' 23 | ) as string; 24 | } 25 | 26 | public enqueue(printable: Printable) { 27 | this.printQueue = [ 28 | ...this.printQueue, 29 | '\n', 30 | '\n', 31 | printable 32 | ]; 33 | } 34 | 35 | public printAndClear() { 36 | const output = this.print(); 37 | this.printQueue = []; 38 | return output; 39 | } 40 | 41 | /** 42 | * When using trailing commas on ObjectTypeProperties within 43 | * ObjectTypeAnnotations, we get weird behavior: 44 | * ``` 45 | * { 46 | * homePlanet: ?string // description 47 | * , 48 | * friends: any // description 49 | * } 50 | * ``` 51 | * when we want 52 | * ``` 53 | * { 54 | * homePlanet: ?string, // description 55 | * friends: any // description 56 | * } 57 | * ``` 58 | */ 59 | private fixCommas(documentPart: string) { 60 | const lines = documentPart.split('\n'); 61 | let currentLine = 0; 62 | let nextLine; 63 | const newDocumentParts = []; 64 | // Keep track of what column comments should start on 65 | // to keep things aligned 66 | let maxCommentColumn = 0; 67 | 68 | while (currentLine !== lines.length) { 69 | nextLine = currentLine + 1; 70 | const strippedNextLine = stripIndent`${lines[nextLine]}`; 71 | if (strippedNextLine.length === 1 && strippedNextLine[0] === ',') { 72 | const currentLineContents = lines[currentLine]; 73 | const commentColumn = currentLineContents.indexOf('//'); 74 | if (maxCommentColumn < commentColumn) { 75 | maxCommentColumn = commentColumn; 76 | } 77 | 78 | const [contents, comment] = currentLineContents.split('//'); 79 | newDocumentParts.push({ 80 | main: contents.replace(/\s+$/g, '') + ',', 81 | comment: comment.trim() 82 | }); 83 | currentLine++; 84 | } else { 85 | newDocumentParts.push({ 86 | main: lines[currentLine], 87 | comment: null 88 | }); 89 | } 90 | 91 | currentLine++; 92 | } 93 | 94 | return newDocumentParts.reduce((memo: string[], part) => { 95 | const { 96 | main, 97 | comment 98 | } = part; 99 | 100 | let line; 101 | if (comment !== null) { 102 | const spacesBetween = maxCommentColumn - main.length; 103 | line = `${main}${' '.repeat(spacesBetween)} // ${comment}` 104 | } else { 105 | line = main; 106 | } 107 | 108 | return [ 109 | ...memo, 110 | line 111 | ]; 112 | }, []).join('\n'); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/utilities/CodeGenerator.ts: -------------------------------------------------------------------------------- 1 | export interface BasicGeneratedFile { 2 | output: string 3 | } 4 | 5 | 6 | export class GeneratedFile implements BasicGeneratedFile { 7 | scopeStack: Scope[] = []; 8 | indentWidth = 2; 9 | indentLevel = 0; 10 | startOfIndentLevel = false; 11 | 12 | public output = ''; 13 | 14 | pushScope(scope: Scope) { 15 | this.scopeStack.push(scope); 16 | } 17 | 18 | popScope() { 19 | return this.scopeStack.pop(); 20 | } 21 | 22 | get scope(): Scope { 23 | if (this.scopeStack.length < 1) throw new Error('No active scope'); 24 | 25 | return this.scopeStack[this.scopeStack.length - 1]; 26 | } 27 | 28 | print(string?: string) { 29 | if (string) { 30 | this.output += string; 31 | } 32 | } 33 | 34 | printNewline() { 35 | if (this.output) { 36 | this.print('\n'); 37 | this.startOfIndentLevel = false; 38 | } 39 | } 40 | 41 | printNewlineIfNeeded() { 42 | if (!this.startOfIndentLevel) { 43 | this.printNewline(); 44 | } 45 | } 46 | 47 | printOnNewline(string?: string) { 48 | if (string) { 49 | this.printNewline(); 50 | this.printIndent(); 51 | this.print(string); 52 | } 53 | } 54 | 55 | printIndent() { 56 | const indentation = ' '.repeat(this.indentLevel * this.indentWidth); 57 | this.output += indentation; 58 | } 59 | 60 | withIndent(closure: Function) { 61 | if (!closure) return; 62 | 63 | this.indentLevel++; 64 | this.startOfIndentLevel = true; 65 | closure(); 66 | this.indentLevel--; 67 | } 68 | 69 | withinBlock(closure: Function, open = ' {', close = '}') { 70 | this.print(open); 71 | this.withIndent(closure); 72 | this.printOnNewline(close); 73 | } 74 | } 75 | 76 | export default class CodeGenerator { 77 | generatedFiles: { [fileName: string]: GeneratedFile } = {}; 78 | currentFile: GeneratedFile; 79 | 80 | constructor(public context: Context) { 81 | this.currentFile = new GeneratedFile(); 82 | } 83 | 84 | withinFile(fileName: string, closure: Function) { 85 | let file = this.generatedFiles[fileName]; 86 | if (!file) { 87 | file = new GeneratedFile(); 88 | this.generatedFiles[fileName] = file; 89 | } 90 | const oldCurrentFile = this.currentFile; 91 | this.currentFile = file; 92 | closure(); 93 | this.currentFile = oldCurrentFile; 94 | } 95 | 96 | get output(): string { 97 | return this.currentFile.output; 98 | } 99 | 100 | pushScope(scope: Scope) { 101 | this.currentFile.pushScope(scope); 102 | } 103 | 104 | popScope() { 105 | this.currentFile.popScope(); 106 | } 107 | 108 | get scope(): Scope { 109 | return this.currentFile.scope; 110 | } 111 | 112 | print(string?: string) { 113 | this.currentFile.print(string); 114 | } 115 | 116 | printNewline() { 117 | this.currentFile.printNewline(); 118 | } 119 | 120 | printNewlineIfNeeded() { 121 | this.currentFile.printNewlineIfNeeded(); 122 | } 123 | 124 | printOnNewline(string?: string) { 125 | this.currentFile.printOnNewline(string); 126 | } 127 | 128 | printIndent() { 129 | this.currentFile.printIndent(); 130 | } 131 | 132 | withIndent(closure: Function) { 133 | this.currentFile.withIndent(closure); 134 | } 135 | 136 | withinBlock(closure: Function, open = ' {', close = '}') { 137 | this.currentFile.withinBlock(closure, open, close); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/flow/language.js: -------------------------------------------------------------------------------- 1 | import { 2 | join, 3 | wrap, 4 | } from '../utilities/printing'; 5 | 6 | import { propertyDeclarations } from './codeGeneration'; 7 | import { typeNameFromGraphQLType } from './types'; 8 | 9 | import { pascalCase } from 'change-case'; 10 | 11 | export function typeDeclaration(generator, { interfaceName, noBrackets }, closure) { 12 | generator.printNewlineIfNeeded(); 13 | generator.printNewline(); 14 | generator.print(`export type ${ interfaceName } = `); 15 | generator.pushScope({ typeName: interfaceName }); 16 | if (noBrackets) { 17 | generator.withinBlock(closure, '', ''); 18 | } else { 19 | generator.withinBlock(closure, '{|', '|}'); 20 | } 21 | generator.popScope(); 22 | generator.print(';'); 23 | } 24 | 25 | export function propertyDeclaration(generator, { 26 | fieldName, 27 | type, 28 | propertyName, 29 | typeName, 30 | description, 31 | isArray, 32 | isNullable, 33 | isArrayElementNullable, 34 | fragmentSpreads, 35 | isInput 36 | }, closure, open = ' {|', close = '|}') { 37 | const name = fieldName || propertyName; 38 | 39 | if (description) { 40 | description.split('\n') 41 | .forEach(line => { 42 | generator.printOnNewline(`// ${line.trim()}`); 43 | }) 44 | } 45 | 46 | if (closure) { 47 | generator.printOnNewline(name) 48 | if (isInput && isNullable) { 49 | generator.print('?') 50 | } 51 | generator.print(':') 52 | if (isNullable) { 53 | generator.print(' ?'); 54 | } 55 | if (isArray) { 56 | if (!isNullable) { 57 | generator.print(' '); 58 | } 59 | generator.print(' Array<'); 60 | if (isArrayElementNullable) { 61 | generator.print('?'); 62 | } 63 | } 64 | 65 | generator.pushScope({ typeName: name }); 66 | 67 | generator.withinBlock(closure, open, close); 68 | 69 | generator.popScope(); 70 | 71 | if (isArray) { 72 | generator.print(' >'); 73 | } 74 | 75 | } else { 76 | generator.printOnNewline(name) 77 | if (isInput && isNullable) { 78 | generator.print('?') 79 | } 80 | generator.print(`: ${typeName || typeNameFromGraphQLType(generator.context, type)}`); 81 | } 82 | generator.print(','); 83 | } 84 | 85 | export function propertySetsDeclaration(generator, property, propertySets, standalone = false) { 86 | const { 87 | description, fieldName, propertyName, typeName, 88 | isNullable, isArray, isArrayElementNullable 89 | } = property; 90 | const name = fieldName || propertyName; 91 | 92 | if (description) { 93 | description.split('\n') 94 | .forEach(line => { 95 | generator.printOnNewline(`// ${line.trim()}`); 96 | }) 97 | } 98 | if (!standalone) { 99 | generator.printOnNewline(`${name}:`); 100 | } 101 | 102 | if (isNullable) { 103 | generator.print(' ?'); 104 | } 105 | 106 | if (isArray) { 107 | generator.print('Array< '); 108 | if (isArrayElementNullable) { 109 | generator.print('?'); 110 | } 111 | } 112 | 113 | generator.pushScope({ typeName: name }); 114 | 115 | generator.withinBlock(() => { 116 | propertySets.forEach((propertySet, index, propertySets) => { 117 | generator.withinBlock(() => { 118 | propertyDeclarations(generator, propertySet); 119 | }); 120 | if (index !== propertySets.length - 1) { 121 | generator.print(' |'); 122 | } 123 | }) 124 | }, '(', ')'); 125 | 126 | generator.popScope(); 127 | 128 | if (isArray) { 129 | generator.print(' >'); 130 | } 131 | 132 | if (!standalone) { 133 | generator.print(','); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /test/jsonOutput.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema, buildSchema, parse } from 'graphql'; 2 | import { compileToLegacyIR } from '../src/compiler/legacyIR'; 3 | import serializeToJSON from '../src/serializeToJSON'; 4 | 5 | import { loadSchema } from '../src/loading'; 6 | const starWarsSchema = loadSchema(require.resolve('./fixtures/starwars/schema.json')); 7 | 8 | function compileFromSource(source: string, schema: GraphQLSchema = starWarsSchema) { 9 | const document = parse(source); 10 | return compileToLegacyIR(schema, document, { 11 | mergeInFieldsFromFragmentSpreads: false, 12 | addTypename: true 13 | }); 14 | } 15 | 16 | describe('JSON output', function() { 17 | test(`should generate JSON output for a query with an enum variable`, function() { 18 | const context = compileFromSource(` 19 | query HeroName($episode: Episode) { 20 | hero(episode: $episode) { 21 | name 22 | } 23 | } 24 | `); 25 | 26 | const output = serializeToJSON(context); 27 | 28 | expect(output).toMatchSnapshot(); 29 | }); 30 | 31 | test(`should generate JSON output for a query with a nested selection set`, function() { 32 | const context = compileFromSource(` 33 | query HeroAndFriendsNames { 34 | hero { 35 | name 36 | friends { 37 | name 38 | } 39 | } 40 | } 41 | `); 42 | 43 | const output = serializeToJSON(context); 44 | 45 | expect(output).toMatchSnapshot(); 46 | }); 47 | 48 | test(`should generate JSON output for a query with a fragment spread and nested inline fragments`, function() { 49 | const context = compileFromSource(` 50 | query HeroAndDetails { 51 | hero { 52 | id 53 | ...CharacterDetails 54 | } 55 | } 56 | 57 | fragment CharacterDetails on Character { 58 | name 59 | ... on Droid { 60 | primaryFunction 61 | } 62 | ... on Human { 63 | height 64 | } 65 | } 66 | `); 67 | 68 | const output = serializeToJSON(context); 69 | 70 | expect(output).toMatchSnapshot(); 71 | }); 72 | 73 | test(`should generate JSON output for a mutation with an enum and an input object variable`, function() { 74 | const context = compileFromSource(` 75 | mutation CreateReview($episode: Episode, $review: ReviewInput) { 76 | createReview(episode: $episode, review: $review) { 77 | stars 78 | commentary 79 | } 80 | } 81 | `); 82 | 83 | const output = serializeToJSON(context); 84 | 85 | expect(output).toMatchSnapshot(); 86 | }); 87 | 88 | test.only(`should generate JSON output for an input object type with default field values`, function() { 89 | const schema = buildSchema(` 90 | type Query { 91 | someField(input: ComplexInput!): String! 92 | } 93 | 94 | input ComplexInput { 95 | string: String = "Hello" 96 | customScalar: Date = "2017-04-16" 97 | listOfString: [String] = ["test1", "test2", "test3"] 98 | listOfInt: [Int] = [1, 2, 3] 99 | listOfEnums: [Episode] = [JEDI, EMPIRE] 100 | listOfCustomScalar: [Date] = ["2017-04-16", "2017-04-17", "2017-04-18"] 101 | } 102 | 103 | scalar Date 104 | 105 | enum Episode { 106 | NEWHOPE 107 | EMPIRE 108 | JEDI 109 | } 110 | `); 111 | 112 | const context = compileFromSource( 113 | ` 114 | query QueryWithComplexInput($input: ComplexInput) { 115 | someField(input: $input) 116 | } 117 | `, 118 | schema 119 | ); 120 | 121 | const output = serializeToJSON(context); 122 | 123 | expect(output).toMatchSnapshot(); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /test/swift/language.ts: -------------------------------------------------------------------------------- 1 | import { stripIndent } from 'common-tags'; 2 | 3 | import { 4 | SwiftGenerator 5 | } from '../../src/swift/language'; 6 | 7 | describe('Swift code generation: Basic language constructs', () => { 8 | let generator: SwiftGenerator; 9 | 10 | beforeEach(() => { 11 | generator = new SwiftGenerator({}); 12 | }); 13 | 14 | it(`should generate a class declaration`, () => { 15 | generator.classDeclaration({ className: 'Hero', modifiers: ['public', 'final'] }, () => { 16 | generator.propertyDeclaration({ propertyName: 'name', typeName: 'String' }); 17 | generator.propertyDeclaration({ propertyName: 'age', typeName: 'Int' }); 18 | }); 19 | 20 | expect(generator.output).toBe(stripIndent` 21 | public final class Hero { 22 | public var name: String 23 | public var age: Int 24 | } 25 | `); 26 | }); 27 | 28 | it(`should generate a struct declaration`, () => { 29 | generator.structDeclaration({ structName: 'Hero' }, () => { 30 | generator.propertyDeclaration({ propertyName: 'name', typeName: 'String' }); 31 | generator.propertyDeclaration({ propertyName: 'age', typeName: 'Int' }); 32 | }); 33 | 34 | expect(generator.output).toBe(stripIndent` 35 | public struct Hero { 36 | public var name: String 37 | public var age: Int 38 | } 39 | `); 40 | }); 41 | 42 | it(`should generate an escaped struct declaration`, () => { 43 | generator.structDeclaration({ structName: 'Type' }, () => { 44 | generator.propertyDeclaration({ propertyName: 'name', typeName: 'String' }); 45 | generator.propertyDeclaration({ propertyName: 'yearOfBirth', typeName: 'Int' }); 46 | }); 47 | 48 | expect(generator.output).toBe(stripIndent` 49 | public struct \`Type\` { 50 | public var name: String 51 | public var yearOfBirth: Int 52 | } 53 | `); 54 | }); 55 | 56 | it(`should generate nested struct declarations`, () => { 57 | generator.structDeclaration({ structName: 'Hero' }, () => { 58 | generator.propertyDeclaration({ propertyName: 'name', typeName: 'String' }); 59 | generator.propertyDeclaration({ propertyName: 'friends', typeName: '[Friend]' }); 60 | 61 | generator.structDeclaration({ structName: 'Friend' }, () => { 62 | generator.propertyDeclaration({ propertyName: 'name', typeName: 'String' }); 63 | }); 64 | }); 65 | 66 | expect(generator.output).toBe(stripIndent` 67 | public struct Hero { 68 | public var name: String 69 | public var friends: [Friend] 70 | 71 | public struct Friend { 72 | public var name: String 73 | } 74 | } 75 | `); 76 | }); 77 | 78 | it(`should generate a protocol declaration`, () => { 79 | generator.protocolDeclaration({ protocolName: 'HeroDetails', adoptedProtocols: ['HasName'] }, () => { 80 | generator.protocolPropertyDeclaration({ propertyName: 'name', typeName: 'String' }); 81 | generator.protocolPropertyDeclaration({ propertyName: 'age', typeName: 'Int' }); 82 | }); 83 | 84 | expect(generator.output).toBe(stripIndent` 85 | public protocol HeroDetails: HasName { 86 | var name: String { get } 87 | var age: Int { get } 88 | } 89 | `); 90 | }); 91 | 92 | it(`should handle multi-line descriptions`, () => { 93 | generator.structDeclaration({ structName: 'Hero', description: 'A hero' }, () => { 94 | generator.propertyDeclaration({ propertyName: 'name', typeName: 'String', description: `A multiline comment \n on the hero's name.` }); 95 | generator.propertyDeclaration({ propertyName: 'age', typeName: 'String', description: `A multiline comment \n on the hero's age.` }); 96 | }); 97 | 98 | expect(generator.output).toMatchSnapshot(); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /test/compiler/visitors/generateOperationId.ts: -------------------------------------------------------------------------------- 1 | import { generateOperationId } from '../../../src/compiler/visitors/generateOperationId'; 2 | import { stripIndent } from 'common-tags'; 3 | 4 | import { compile } from '../../test-utils/helpers'; 5 | 6 | describe(`generateOperationId()`, () => { 7 | it(`should generate different operation IDs for different operations`, () => { 8 | const context1 = compile(` 9 | query Hero { 10 | hero { 11 | ...HeroDetails 12 | } 13 | } 14 | fragment HeroDetails on Character { 15 | name 16 | } 17 | `); 18 | 19 | const { operationId: id1 } = generateOperationId(context1.operations['Hero'], context1.fragments); 20 | 21 | const context2 = compile(` 22 | query Hero { 23 | hero { 24 | ...HeroDetails 25 | } 26 | } 27 | fragment HeroDetails on Character { 28 | appearsIn 29 | } 30 | `); 31 | 32 | const { operationId: id2 } = generateOperationId(context2.operations['Hero'], context2.fragments); 33 | 34 | expect(id1).not.toBe(id2); 35 | }); 36 | 37 | it(`should generate the same operation ID regardless of operation formatting/commenting`, () => { 38 | const context1 = compile(` 39 | query HeroName($episode: Episode) { 40 | hero(episode: $episode) { 41 | name 42 | } 43 | } 44 | `); 45 | 46 | const { operationId: id1 } = generateOperationId(context1.operations['HeroName'], context1.fragments); 47 | 48 | const context2 = compile(` 49 | # Profound comment 50 | query HeroName($episode:Episode) { hero(episode: $episode) { name } } 51 | # Deeply meaningful comment 52 | `); 53 | 54 | const { operationId: id2 } = generateOperationId(context2.operations['HeroName'], context2.fragments); 55 | 56 | expect(id1).toBe(id2); 57 | }); 58 | 59 | it(`should generate the same operation ID regardless of fragment order`, () => { 60 | const context1 = compile(` 61 | query Hero { 62 | hero { 63 | ...HeroName 64 | ...HeroAppearsIn 65 | } 66 | } 67 | fragment HeroName on Character { 68 | name 69 | } 70 | fragment HeroAppearsIn on Character { 71 | appearsIn 72 | } 73 | `); 74 | 75 | const { operationId: id1 } = generateOperationId(context1.operations['Hero'], context1.fragments); 76 | 77 | const context2 = compile(` 78 | query Hero { 79 | hero { 80 | ...HeroName 81 | ...HeroAppearsIn 82 | } 83 | } 84 | fragment HeroAppearsIn on Character { 85 | appearsIn 86 | } 87 | fragment HeroName on Character { 88 | name 89 | } 90 | `); 91 | 92 | const { operationId: id2 } = generateOperationId(context2.operations['Hero'], context2.fragments); 93 | 94 | expect(id1).toBe(id2); 95 | }); 96 | 97 | it(`should generate appropriate operation ID mapping source when there are nested fragment references`, () => { 98 | const context = compile(` 99 | query Hero { 100 | hero { 101 | ...HeroDetails 102 | } 103 | } 104 | fragment HeroName on Character { 105 | name 106 | } 107 | fragment HeroDetails on Character { 108 | ...HeroName 109 | appearsIn 110 | } 111 | `); 112 | 113 | const { sourceWithFragments } = generateOperationId(context.operations['Hero'], context.fragments); 114 | 115 | expect(sourceWithFragments).toBe(stripIndent` 116 | query Hero { 117 | hero { 118 | ...HeroDetails 119 | } 120 | } 121 | fragment HeroDetails on Character { 122 | ...HeroName 123 | appearsIn 124 | } 125 | fragment HeroName on Character { 126 | name 127 | } 128 | `); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /test/scala/types.js: -------------------------------------------------------------------------------- 1 | import { stripIndent } from 'common-tags' 2 | 3 | import { 4 | GraphQLString, 5 | GraphQLInt, 6 | GraphQLFloat, 7 | GraphQLBoolean, 8 | GraphQLID, 9 | GraphQLList, 10 | GraphQLNonNull, 11 | GraphQLScalarType, 12 | } from 'graphql'; 13 | 14 | import { loadSchema } from '../../src/loading' 15 | const schema = loadSchema(require.resolve('../fixtures/starwars/schema.json')); 16 | 17 | import CodeGenerator from '../../src/utilities/CodeGenerator'; 18 | 19 | import { typeNameFromGraphQLType } from '../../src/scala/types' 20 | 21 | describe('Scala code generation: Types', function() { 22 | describe('#typeNameFromGraphQLType()', function() { 23 | test('should return Option[String] for GraphQLString', function() { 24 | expect(typeNameFromGraphQLType({}, GraphQLString)).toBe('Option[String]'); 25 | }); 26 | 27 | test('should return String for GraphQLNonNull(GraphQLString)', function() { 28 | expect(typeNameFromGraphQLType({}, new GraphQLNonNull(GraphQLString))).toBe('String'); 29 | }); 30 | 31 | test('should return Option[Seq[Option[String]]] for GraphQLList(GraphQLString)', function() { 32 | expect(typeNameFromGraphQLType({}, new GraphQLList(GraphQLString))).toBe('Option[Seq[Option[String]]]'); 33 | }); 34 | 35 | test('should return Seq[String] for GraphQLNonNull(GraphQLList(GraphQLString))', function() { 36 | expect(typeNameFromGraphQLType({}, new GraphQLNonNull(new GraphQLList(GraphQLString)))).toBe('Seq[Option[String]]'); 37 | }); 38 | 39 | test('should return Option[Seq[String]] for GraphQLList(GraphQLNonNull(GraphQLString))', function() { 40 | expect(typeNameFromGraphQLType({}, new GraphQLList(new GraphQLNonNull(GraphQLString)))).toBe('Option[Seq[String]]'); 41 | }); 42 | 43 | test('should return Seq[String] for GraphQLNonNull(GraphQLList(GraphQLNonNull(GraphQLString)))', function() { 44 | expect(typeNameFromGraphQLType({}, new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLString))))).toBe('Seq[String]'); 45 | }); 46 | 47 | test('should return Option[Seq[Option[Seq[Option[String]]]]] for GraphQLList(GraphQLList(GraphQLString))', function() { 48 | expect(typeNameFromGraphQLType({}, new GraphQLList(new GraphQLList(GraphQLString)))).toBe('Option[Seq[Option[Seq[Option[String]]]]]'); 49 | }); 50 | 51 | test('should return Option[Seq[Seq[Option[String]]]] for GraphQLList(GraphQLNonNull(GraphQLList(GraphQLString)))', function() { 52 | expect(typeNameFromGraphQLType({}, new GraphQLList(new GraphQLNonNull(new GraphQLList(GraphQLString))))).toBe('Option[Seq[Seq[Option[String]]]]'); 53 | }); 54 | 55 | test('should return Option[Int] for GraphQLInt', function() { 56 | expect(typeNameFromGraphQLType({}, GraphQLInt)).toBe('Option[Int]'); 57 | }); 58 | 59 | test('should return Option[Double] for GraphQLFloat', function() { 60 | expect(typeNameFromGraphQLType({}, GraphQLFloat)).toBe('Option[Double]'); 61 | }); 62 | 63 | test('should return Option[Boolean] for GraphQLBoolean', function() { 64 | expect(typeNameFromGraphQLType({}, GraphQLBoolean)).toBe('Option[Boolean]'); 65 | }); 66 | 67 | test('should return Option[String] for GraphQLID', function() { 68 | expect(typeNameFromGraphQLType({}, GraphQLID)).toBe('Option[String]'); 69 | }); 70 | 71 | test('should return Option[String] for a custom scalar type', function() { 72 | expect(typeNameFromGraphQLType({}, new GraphQLScalarType({ name: 'CustomScalarType', serialize: String }))).toBe('Option[String]'); 73 | }); 74 | 75 | test('should return a passed through custom scalar type with the passthroughCustomScalars option', function() { 76 | expect(typeNameFromGraphQLType({ passthroughCustomScalars: true, customScalarsPrefix: '' }, new GraphQLScalarType({ name: 'CustomScalarType', serialize: String }))).toBe('Option[CustomScalarType]'); 77 | }); 78 | 79 | test('should return a passed through custom scalar type with a prefix with the customScalarsPrefix option', function() { 80 | expect(typeNameFromGraphQLType({ passthroughCustomScalars: true, customScalarsPrefix: 'My' }, new GraphQLScalarType({ name: 'CustomScalarType', serialize: String }))).toBe('Option[MyCustomScalarType]'); 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /src/typescript/language.ts: -------------------------------------------------------------------------------- 1 | import { LegacyInlineFragment } from '../compiler/legacyIR'; 2 | 3 | import { propertyDeclarations } from './codeGeneration'; 4 | import { typeNameFromGraphQLType } from './types'; 5 | 6 | import CodeGenerator from "../utilities/CodeGenerator"; 7 | import { GraphQLType } from "graphql"; 8 | 9 | export interface Property { 10 | fieldName?: string, 11 | fieldType?: GraphQLType, 12 | propertyName?: string, 13 | type?: GraphQLType, 14 | description?: string, 15 | typeName?: string, 16 | isComposite?: boolean, 17 | isNullable?: boolean, 18 | fields?: any[], 19 | inlineFragments?: LegacyInlineFragment[], 20 | fragmentSpreads?: any, 21 | isInput?: boolean, 22 | isArray?: boolean, 23 | isArrayElementNullable?: boolean | null, 24 | } 25 | 26 | export function interfaceDeclaration(generator: CodeGenerator, { 27 | interfaceName, 28 | noBrackets 29 | }: { interfaceName: string, noBrackets?: boolean }, 30 | closure: () => void) { 31 | generator.printNewlineIfNeeded(); 32 | generator.printNewline(); 33 | generator.print(`export type ${interfaceName} = `); 34 | generator.pushScope({ typeName: interfaceName }); 35 | if (noBrackets) { 36 | generator.withinBlock(closure, '', ''); 37 | } else { 38 | generator.withinBlock(closure, '{', '}'); 39 | } 40 | generator.popScope(); 41 | generator.print(';'); 42 | } 43 | 44 | export function propertyDeclaration(generator: CodeGenerator, { 45 | fieldName, 46 | type, 47 | propertyName, 48 | typeName, 49 | description, 50 | isInput, 51 | isArray, 52 | isNullable, 53 | isArrayElementNullable 54 | }: Property, closure?: () => void) { 55 | const name = fieldName || propertyName; 56 | 57 | if (description) { 58 | description.split('\n') 59 | .forEach(line => { 60 | generator.printOnNewline(`// ${line.trim()}`); 61 | }) 62 | } 63 | 64 | if (closure) { 65 | generator.printOnNewline(name); 66 | 67 | if (isNullable && isInput) { 68 | generator.print('?'); 69 | } 70 | generator.print(': '); 71 | 72 | if (isArray) { 73 | generator.print(' Array<'); 74 | } 75 | generator.pushScope({ typeName: name }); 76 | 77 | generator.withinBlock(closure); 78 | 79 | generator.popScope(); 80 | 81 | if (isArray) { 82 | if (isArrayElementNullable) { 83 | generator.print(' | null'); 84 | } 85 | generator.print(' >'); 86 | } 87 | 88 | if (isNullable) { 89 | generator.print(' | null'); 90 | } 91 | 92 | } else { 93 | generator.printOnNewline(name); 94 | if (isInput && isNullable) { 95 | generator.print('?') 96 | } 97 | generator.print(`: ${typeName || type && typeNameFromGraphQLType(generator.context, type)}`); 98 | } 99 | generator.print(','); 100 | } 101 | 102 | export function propertySetsDeclaration(generator: CodeGenerator, property: Property, propertySets: Property[][], standalone = false) { 103 | const { 104 | description, fieldName, propertyName, 105 | isNullable, isArray, isArrayElementNullable, 106 | } = property; 107 | const name = fieldName || propertyName; 108 | 109 | if (description) { 110 | description.split('\n') 111 | .forEach(line => { 112 | generator.printOnNewline(`// ${line.trim()}`); 113 | }) 114 | } 115 | 116 | if (!standalone) { 117 | generator.printOnNewline(`${name}: `); 118 | } 119 | 120 | if (isArray) { 121 | generator.print(' Array<'); 122 | } 123 | 124 | generator.pushScope({ typeName: name }); 125 | 126 | generator.withinBlock(() => { 127 | propertySets.forEach((propertySet, index, propertySets) => { 128 | generator.withinBlock(() => { 129 | propertyDeclarations(generator, propertySet); 130 | }); 131 | if (index !== propertySets.length - 1) { 132 | generator.print(' |'); 133 | } 134 | }) 135 | }, '(', ')'); 136 | 137 | generator.popScope(); 138 | 139 | if (isArray) { 140 | if (isArrayElementNullable) { 141 | generator.print(' | null'); 142 | } 143 | generator.print(' >'); 144 | } 145 | 146 | if (isNullable) { 147 | generator.print(' | null'); 148 | } 149 | 150 | if (!standalone) { 151 | generator.print(','); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /test/swift/typeNameFromGraphQLType.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLString, 3 | GraphQLInt, 4 | GraphQLFloat, 5 | GraphQLBoolean, 6 | GraphQLID, 7 | GraphQLList, 8 | GraphQLNonNull, 9 | GraphQLScalarType 10 | } from 'graphql'; 11 | 12 | import { Helpers } from '../../src/swift/helpers'; 13 | 14 | describe('Swift code generation: Types', () => { 15 | let helpers: Helpers; 16 | 17 | beforeEach(() => { 18 | helpers = new Helpers({}); 19 | }); 20 | 21 | describe('#typeNameFromGraphQLType()', () => { 22 | it('should return String? for GraphQLString', () => { 23 | expect(helpers.typeNameFromGraphQLType(GraphQLString)).toBe('String?'); 24 | }); 25 | 26 | it('should return String for GraphQLNonNull(GraphQLString)', () => { 27 | expect(helpers.typeNameFromGraphQLType(new GraphQLNonNull(GraphQLString))).toBe('String'); 28 | }); 29 | 30 | it('should return [String?]? for GraphQLList(GraphQLString)', () => { 31 | expect(helpers.typeNameFromGraphQLType(new GraphQLList(GraphQLString))).toBe('[String?]?'); 32 | }); 33 | 34 | it('should return [String?] for GraphQLNonNull(GraphQLList(GraphQLString))', () => { 35 | expect(helpers.typeNameFromGraphQLType(new GraphQLNonNull(new GraphQLList(GraphQLString)))).toBe( 36 | '[String?]' 37 | ); 38 | }); 39 | 40 | it('should return [String]? for GraphQLList(GraphQLNonNull(GraphQLString))', () => { 41 | expect(helpers.typeNameFromGraphQLType(new GraphQLList(new GraphQLNonNull(GraphQLString)))).toBe( 42 | '[String]?' 43 | ); 44 | }); 45 | 46 | it('should return [String] for GraphQLNonNull(GraphQLList(GraphQLNonNull(GraphQLString)))', () => { 47 | expect( 48 | helpers.typeNameFromGraphQLType( 49 | new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLString))) 50 | ) 51 | ).toBe('[String]'); 52 | }); 53 | 54 | it('should return [[String?]?]? for GraphQLList(GraphQLList(GraphQLString))', () => { 55 | expect(helpers.typeNameFromGraphQLType(new GraphQLList(new GraphQLList(GraphQLString)))).toBe( 56 | '[[String?]?]?' 57 | ); 58 | }); 59 | 60 | it('should return [[String?]]? for GraphQLList(GraphQLNonNull(GraphQLList(GraphQLString)))', () => { 61 | expect( 62 | helpers.typeNameFromGraphQLType(new GraphQLList(new GraphQLNonNull(new GraphQLList(GraphQLString)))) 63 | ).toBe('[[String?]]?'); 64 | }); 65 | 66 | it('should return Int? for GraphQLInt', () => { 67 | expect(helpers.typeNameFromGraphQLType(GraphQLInt)).toBe('Int?'); 68 | }); 69 | 70 | it('should return Double? for GraphQLFloat', () => { 71 | expect(helpers.typeNameFromGraphQLType(GraphQLFloat)).toBe('Double?'); 72 | }); 73 | 74 | it('should return Bool? for GraphQLBoolean', () => { 75 | expect(helpers.typeNameFromGraphQLType(GraphQLBoolean)).toBe('Bool?'); 76 | }); 77 | 78 | it('should return GraphQLID? for GraphQLID', () => { 79 | expect(helpers.typeNameFromGraphQLType(GraphQLID)).toBe('GraphQLID?'); 80 | }); 81 | 82 | it('should return String? for a custom scalar type', () => { 83 | expect( 84 | helpers.typeNameFromGraphQLType( 85 | new GraphQLScalarType({ name: 'CustomScalarType', serialize: String }) 86 | ) 87 | ).toBe('String?'); 88 | }); 89 | 90 | it('should return a passed through custom scalar type with the passthroughCustomScalars option', () => { 91 | helpers.options.passthroughCustomScalars = true; 92 | helpers.options.customScalarsPrefix = ''; 93 | 94 | expect( 95 | helpers.typeNameFromGraphQLType( 96 | new GraphQLScalarType({ name: 'CustomScalarType', serialize: String }) 97 | ) 98 | ).toBe('CustomScalarType?'); 99 | }); 100 | 101 | it('should return a passed through custom scalar type with a prefix with the customScalarsPrefix option', () => { 102 | helpers.options.passthroughCustomScalars = true; 103 | helpers.options.customScalarsPrefix = 'My'; 104 | 105 | expect( 106 | helpers.typeNameFromGraphQLType( 107 | new GraphQLScalarType({ name: 'CustomScalarType', serialize: String }) 108 | ) 109 | ).toBe('MyCustomScalarType?'); 110 | }); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /test/fixtures/starwars/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: Query 3 | mutation: Mutation 4 | } 5 | # The query type, represents all of the entry points into our object graph 6 | type Query { 7 | hero(episode: Episode): Character 8 | reviews(episode: Episode!): [Review] 9 | search(text: String): [SearchResult] 10 | character(id: ID!): Character 11 | droid(id: ID!): Droid 12 | human(id: ID!): Human 13 | starship(id: ID!): Starship 14 | } 15 | # The mutation type, represents all updates we can make to our data 16 | type Mutation { 17 | createReview(episode: Episode, review: ReviewInput!): Review 18 | } 19 | # The episodes in the Star Wars trilogy 20 | enum Episode { 21 | # Star Wars Episode IV: A New Hope, released in 1977. 22 | NEWHOPE 23 | # Star Wars Episode V: The Empire Strikes Back, released in 1980. 24 | EMPIRE 25 | # Star Wars Episode VI: Return of the Jedi, released in 1983. 26 | JEDI 27 | } 28 | # A character from the Star Wars universe 29 | interface Character { 30 | # The ID of the character 31 | id: ID! 32 | # The name of the character 33 | name: String! 34 | # The friends of the character, or an empty list if they have none 35 | friends: [Character] 36 | # The friends of the character exposed as a connection with edges 37 | friendsConnection(first: Int, after: ID): FriendsConnection! 38 | # The movies this character appears in 39 | appearsIn: [Episode]! 40 | } 41 | # Units of height 42 | enum LengthUnit { 43 | # The standard unit around the world 44 | METER 45 | # Primarily used in the United States 46 | FOOT 47 | } 48 | # A humanoid creature from the Star Wars universe 49 | type Human implements Character { 50 | # The ID of the human 51 | id: ID! 52 | # What this human calls themselves 53 | name: String! 54 | # The home planet of the human, or null if unknown 55 | homePlanet: String 56 | # Height in the preferred unit, default is meters 57 | height(unit: LengthUnit = METER): Float 58 | # Mass in kilograms, or null if unknown 59 | mass: Float 60 | # This human's friends, or an empty list if they have none 61 | friends: [Character] 62 | # The friends of the human exposed as a connection with edges 63 | friendsConnection(first: Int, after: ID): FriendsConnection! 64 | # The movies this human appears in 65 | appearsIn: [Episode]! 66 | # A list of starships this person has piloted, or an empty list if none 67 | starships: [Starship] 68 | } 69 | # An autonomous mechanical character in the Star Wars universe 70 | type Droid implements Character { 71 | # The ID of the droid 72 | id: ID! 73 | # What others call this droid 74 | name: String! 75 | # This droid's friends, or an empty list if they have none 76 | friends: [Character] 77 | # The friends of the droid exposed as a connection with edges 78 | friendsConnection(first: Int, after: ID): FriendsConnection! 79 | # The movies this droid appears in 80 | appearsIn: [Episode]! 81 | # This droid's primary function 82 | primaryFunction: String 83 | } 84 | # A connection object for a character's friends 85 | type FriendsConnection { 86 | # The total number of friends 87 | totalCount: Int 88 | # The edges for each of the character's friends. 89 | edges: [FriendsEdge] 90 | # A list of the friends, as a convenience when edges are not needed. 91 | friends: [Character] 92 | # Information for paginating this connection 93 | pageInfo: PageInfo! 94 | } 95 | # An edge object for a character's friends 96 | type FriendsEdge { 97 | # A cursor used for pagination 98 | cursor: ID! 99 | # The character represented by this friendship edge 100 | node: Character 101 | } 102 | # Information for paginating this connection 103 | type PageInfo { 104 | startCursor: ID 105 | endCursor: ID 106 | hasNextPage: Boolean! 107 | } 108 | # Represents a review for a movie 109 | type Review { 110 | # The number of stars this review gave, 1-5 111 | stars: Int! 112 | # Comment about the movie 113 | commentary: String 114 | } 115 | # The input object sent when someone is creating a new review 116 | input ReviewInput { 117 | # 0-5 stars 118 | stars: Int! 119 | # Comment about the movie, optional 120 | commentary: String 121 | # Favorite color, optional 122 | favorite_color: ColorInput 123 | } 124 | # The input object sent when passing in a color 125 | input ColorInput { 126 | red: Int! 127 | green: Int! 128 | blue: Int! 129 | } 130 | type Starship { 131 | # The ID of the starship 132 | id: ID! 133 | # The name of the starship 134 | name: String! 135 | # Length of the starship, along the longest axis 136 | length(unit: LengthUnit = METER): Float 137 | coordinates: [[Float!]!] 138 | } 139 | union SearchResult = Human | Droid | Starship -------------------------------------------------------------------------------- /src/flow-modern/language.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLEnumType, 3 | GraphQLInputObjectType, 4 | } from 'graphql'; 5 | 6 | import { 7 | CompilerOptions 8 | } from '../compiler'; 9 | 10 | import { createTypeAnnotationFromGraphQLTypeFunction } from './helpers'; 11 | 12 | import * as t from '@babel/types'; 13 | 14 | export type ObjectProperty = { 15 | name: string, 16 | description?: string | null | undefined, 17 | annotation: t.FlowTypeAnnotation 18 | } 19 | 20 | export interface FlowCompilerOptions extends CompilerOptions { 21 | useFlowExactObjects: boolean 22 | } 23 | 24 | export default class FlowGenerator { 25 | options: FlowCompilerOptions 26 | typeAnnotationFromGraphQLType: Function 27 | 28 | constructor(compilerOptions: FlowCompilerOptions) { 29 | this.options = compilerOptions; 30 | 31 | this.typeAnnotationFromGraphQLType = createTypeAnnotationFromGraphQLTypeFunction(compilerOptions); 32 | } 33 | 34 | public enumerationDeclaration(type: GraphQLEnumType) { 35 | const { name, description } = type; 36 | const unionValues = type.getValues().map(({ value }) => { 37 | const type = t.stringLiteralTypeAnnotation(); 38 | type.value = value; 39 | 40 | return type; 41 | }); 42 | 43 | const typeAlias = t.exportNamedDeclaration( 44 | t.typeAlias( 45 | t.identifier(name), 46 | undefined, 47 | t.unionTypeAnnotation(unionValues) 48 | ), 49 | [] 50 | ); 51 | 52 | typeAlias.leadingComments = [{ 53 | type: 'CommentLine', 54 | value: ` ${description}` 55 | } as t.CommentLine]; 56 | 57 | return typeAlias; 58 | } 59 | 60 | public inputObjectDeclaration(inputObjectType: GraphQLInputObjectType) { 61 | const { name, description } = inputObjectType; 62 | 63 | const fieldMap = inputObjectType.getFields(); 64 | const fields: ObjectProperty[] = Object.keys(inputObjectType.getFields()) 65 | .map((fieldName: string) => { 66 | const field = fieldMap[fieldName]; 67 | return { 68 | name: fieldName, 69 | annotation: this.typeAnnotationFromGraphQLType(field.type) 70 | } 71 | }); 72 | 73 | const typeAlias = this.typeAliasObject(name, fields); 74 | 75 | typeAlias.leadingComments = [{ 76 | type: 'CommentLine', 77 | value: ` ${description}` 78 | } as t.CommentLine] 79 | 80 | return typeAlias; 81 | } 82 | 83 | public objectTypeAnnotation(fields: ObjectProperty[], isInputObject: boolean = false) { 84 | const objectTypeAnnotation = t.objectTypeAnnotation( 85 | fields.map(({name, description, annotation}) => { 86 | if (annotation.type === "NullableTypeAnnotation") { 87 | t.identifier(name + '?') 88 | } 89 | 90 | const objectTypeProperty = t.objectTypeProperty( 91 | t.identifier( 92 | // Nullable fields on input objects do not have to be defined 93 | // as well, so allow these fields to be "undefined" 94 | (isInputObject && annotation.type === "NullableTypeAnnotation") 95 | ? name + '?' 96 | : name 97 | ), 98 | annotation 99 | ); 100 | 101 | if (description) { 102 | objectTypeProperty.trailingComments = [{ 103 | type: 'CommentLine', 104 | value: ` ${description}` 105 | } as t.CommentLine] 106 | } 107 | 108 | return objectTypeProperty; 109 | }) 110 | ); 111 | 112 | if (this.options.useFlowExactObjects) { 113 | objectTypeAnnotation.exact = true; 114 | } 115 | 116 | return objectTypeAnnotation; 117 | } 118 | 119 | public typeAliasObject(name: string, fields: ObjectProperty[]) { 120 | return t.typeAlias( 121 | t.identifier(name), 122 | undefined, 123 | this.objectTypeAnnotation(fields) 124 | ); 125 | } 126 | 127 | public typeAliasObjectUnion(name: string, members: ObjectProperty[][]) { 128 | return t.typeAlias( 129 | t.identifier(name), 130 | undefined, 131 | t.unionTypeAnnotation( 132 | members.map(member => { 133 | return this.objectTypeAnnotation(member) 134 | }) 135 | ) 136 | ) 137 | } 138 | 139 | public typeAliasGenericUnion(name: string, members: t.FlowTypeAnnotation[]) { 140 | return t.typeAlias( 141 | t.identifier(name), 142 | undefined, 143 | t.unionTypeAnnotation(members) 144 | ); 145 | } 146 | 147 | public exportDeclaration(declaration: t.Declaration) { 148 | return t.exportNamedDeclaration(declaration, []); 149 | } 150 | 151 | public annotationFromScopeStack(scope: string[]) { 152 | return t.genericTypeAnnotation( 153 | t.identifier( 154 | scope.join('_') 155 | ) 156 | ); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/generate.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import * as rimraf from 'rimraf'; 4 | 5 | import { loadSchema, loadSchemaFromConfig, loadAndMergeQueryDocuments } from './loading'; 6 | import { validateQueryDocument } from './validation'; 7 | import { compileToIR } from './compiler'; 8 | import { compileToLegacyIR } from './compiler/legacyIR'; 9 | import serializeToJSON from './serializeToJSON'; 10 | import { BasicGeneratedFile } from './utilities/CodeGenerator' 11 | import { generateSource as generateSwiftSource } from './swift'; 12 | import { generateSource as generateTypescriptSource } from './typescript'; 13 | import { generateSource as generateFlowSource } from './flow'; 14 | import { generateSource as generateFlowModernSource } from './flow-modern'; 15 | import { generateSource as generateScalaSource } from './scala'; 16 | 17 | type TargetType = 'json' | 'swift' | 'ts' | 'typescript' | 'flow' | 'scala' | 'flow-modern'; 18 | 19 | export default function generate( 20 | inputPaths: string[], 21 | schemaPath: string, 22 | outputPath: string, 23 | only: string, 24 | target: TargetType, 25 | tagName: string, 26 | projectName: string, 27 | options: any 28 | ) { 29 | const schema = schemaPath == null 30 | ? loadSchemaFromConfig(projectName) 31 | : loadSchema(schemaPath); 32 | 33 | const document = loadAndMergeQueryDocuments(inputPaths, tagName); 34 | 35 | validateQueryDocument(schema, document); 36 | 37 | if (target === 'swift') { 38 | options.addTypename = true; 39 | const context = compileToIR(schema, document, options); 40 | 41 | const outputIndividualFiles = fs.existsSync(outputPath) && fs.statSync(outputPath).isDirectory(); 42 | 43 | const generator = generateSwiftSource(context, outputIndividualFiles, only); 44 | 45 | if (outputIndividualFiles) { 46 | writeGeneratedFiles(generator.generatedFiles, outputPath); 47 | } else { 48 | fs.writeFileSync(outputPath, generator.output); 49 | } 50 | 51 | if (options.generateOperationIds) { 52 | writeOperationIdsMap(context); 53 | } 54 | } 55 | else if (target === 'flow-modern') { 56 | const context = compileToIR(schema, document, options); 57 | const generatedFiles = generateFlowModernSource(context); 58 | 59 | // Group by output directory 60 | const filesByOutputDirectory: { 61 | [outputDirectory: string]: { 62 | [fileName: string]: BasicGeneratedFile 63 | } 64 | } = {}; 65 | 66 | Object.keys(generatedFiles) 67 | .forEach((filePath: string) => { 68 | const outputDirectory = path.dirname(filePath); 69 | if (!filesByOutputDirectory[outputDirectory]) { 70 | filesByOutputDirectory[outputDirectory] = { 71 | [path.basename(filePath)]: generatedFiles[filePath] 72 | }; 73 | } else { 74 | filesByOutputDirectory[outputDirectory][path.basename(filePath)] = generatedFiles[filePath]; 75 | } 76 | }) 77 | 78 | Object.keys(filesByOutputDirectory) 79 | .forEach((outputDirectory) => { 80 | writeGeneratedFiles( 81 | filesByOutputDirectory[outputDirectory], 82 | outputDirectory 83 | ); 84 | }); 85 | } 86 | else { 87 | let output; 88 | const context = compileToLegacyIR(schema, document, options); 89 | switch (target) { 90 | case 'json': 91 | output = serializeToJSON(context); 92 | break; 93 | case 'ts': 94 | case 'typescript': 95 | output = generateTypescriptSource(context); 96 | break; 97 | case 'flow': 98 | output = generateFlowSource(context); 99 | break; 100 | case 'scala': 101 | output = generateScalaSource(context, options); 102 | } 103 | 104 | if (outputPath) { 105 | fs.writeFileSync(outputPath, output); 106 | } else { 107 | console.log(output); 108 | } 109 | } 110 | } 111 | 112 | function writeGeneratedFiles( 113 | generatedFiles: { [fileName: string]: BasicGeneratedFile }, 114 | outputDirectory: string 115 | ) { 116 | // Clear all generated stuff to make sure there isn't anything 117 | // unnecessary lying around. 118 | rimraf.sync(outputDirectory); 119 | // Remake the output directory 120 | fs.mkdirSync(outputDirectory); 121 | 122 | for (const [fileName, generatedFile] of Object.entries(generatedFiles)) { 123 | fs.writeFileSync(path.join(outputDirectory, fileName), generatedFile.output); 124 | } 125 | } 126 | 127 | interface OperationIdsMap { 128 | name: string; 129 | source: string; 130 | } 131 | 132 | function writeOperationIdsMap(context: any) { 133 | let operationIdsMap: { [id: string]: OperationIdsMap } = {}; 134 | Object.values(context.operations).forEach(operation => { 135 | operationIdsMap[operation.operationId] = { 136 | name: operation.operationName, 137 | source: operation.sourceWithFragments 138 | }; 139 | }); 140 | fs.writeFileSync(context.options.operationIdsPath, JSON.stringify(operationIdsMap, null, 2)); 141 | } 142 | -------------------------------------------------------------------------------- /src/compiler/visitors/collectAndMergeFields.ts: -------------------------------------------------------------------------------- 1 | import { SelectionSet, Selection, Field, BooleanCondition } from '../'; 2 | import { GraphQLObjectType } from 'graphql'; 3 | 4 | // This is a temporary workaround to keep track of conditions on fields in the fields themselves. 5 | // It is only added here because we want to expose it to the Android target, which relies on the legacy IR. 6 | declare module '../' { 7 | interface Field { 8 | conditions?: BooleanCondition[]; 9 | } 10 | } 11 | 12 | export function collectAndMergeFields( 13 | selectionSet: SelectionSet, 14 | mergeInFragmentSpreads: Boolean = true 15 | ): Field[] { 16 | const groupedFields: Map = new Map(); 17 | 18 | function visitSelectionSet( 19 | selections: Selection[], 20 | possibleTypes: GraphQLObjectType[], 21 | conditions: BooleanCondition[] = [] 22 | ) { 23 | if (possibleTypes.length < 1) return; 24 | 25 | for (const selection of selections) { 26 | switch (selection.kind) { 27 | case 'Field': 28 | let groupForResponseKey = groupedFields.get(selection.responseKey); 29 | if (!groupForResponseKey) { 30 | groupForResponseKey = []; 31 | groupedFields.set(selection.responseKey, groupForResponseKey); 32 | } 33 | // Make sure to deep clone selections to avoid modifying the original field 34 | // TODO: Should we use an object freezing / immutability solution? 35 | groupForResponseKey.push({ 36 | ...selection, 37 | isConditional: conditions.length > 0, 38 | conditions, 39 | selectionSet: selection.selectionSet 40 | ? { 41 | possibleTypes: selection.selectionSet.possibleTypes, 42 | selections: [...selection.selectionSet.selections] 43 | } 44 | : undefined 45 | }); 46 | break; 47 | case 'FragmentSpread': 48 | case 'TypeCondition': 49 | if (selection.kind === 'FragmentSpread' && !mergeInFragmentSpreads) continue; 50 | 51 | // Only merge fragment spreads and type conditions if they match all possible types. 52 | if (!possibleTypes.every(type => selection.selectionSet.possibleTypes.includes(type))) continue; 53 | 54 | visitSelectionSet(selection.selectionSet.selections, possibleTypes, conditions); 55 | break; 56 | case 'BooleanCondition': 57 | visitSelectionSet(selection.selectionSet.selections, possibleTypes, [...conditions, selection]); 58 | break; 59 | } 60 | } 61 | } 62 | 63 | visitSelectionSet(selectionSet.selections, selectionSet.possibleTypes); 64 | 65 | // Merge selection sets 66 | 67 | const fields = Array.from(groupedFields.values()).map(fields => { 68 | const isFieldIncludedUnconditionally = fields.some(field => !field.isConditional); 69 | 70 | return fields 71 | .map(field => { 72 | if (isFieldIncludedUnconditionally && field.isConditional && field.selectionSet) { 73 | field.selectionSet.selections = wrapInBooleanConditionsIfNeeded( 74 | field.selectionSet.selections, 75 | field.conditions 76 | ); 77 | } 78 | return field; 79 | }) 80 | .reduce((field, otherField) => { 81 | field.isConditional = field.isConditional && otherField.isConditional; 82 | 83 | // FIXME: This is strictly taken incorrect, because the conditions should be ORed 84 | // These conditions are only used in Android target however, 85 | // and there is now way to express this in the legacy IR. 86 | if (field.conditions && otherField.conditions) { 87 | field.conditions = [...field.conditions, ...otherField.conditions]; 88 | } else { 89 | field.conditions = undefined; 90 | } 91 | 92 | if (field.selectionSet && otherField.selectionSet) { 93 | field.selectionSet.selections.push(...otherField.selectionSet.selections); 94 | } 95 | 96 | return field; 97 | }); 98 | }); 99 | 100 | // Replace field descriptions with type-specific descriptions if possible 101 | if (selectionSet.possibleTypes.length == 1) { 102 | const type = selectionSet.possibleTypes[0]; 103 | const fieldDefMap = type.getFields(); 104 | 105 | for (const field of fields) { 106 | const fieldDef = fieldDefMap[field.name]; 107 | 108 | if (fieldDef && fieldDef.description) { 109 | field.description = fieldDef.description; 110 | } 111 | } 112 | } 113 | 114 | return fields; 115 | } 116 | 117 | export function wrapInBooleanConditionsIfNeeded( 118 | selections: Selection[], 119 | conditions?: BooleanCondition[] 120 | ): Selection[] { 121 | if (!conditions || conditions.length == 0) return selections; 122 | 123 | const [condition, ...rest] = conditions; 124 | return [ 125 | { 126 | ...condition, 127 | selectionSet: { 128 | possibleTypes: condition.selectionSet.possibleTypes, 129 | selections: wrapInBooleanConditionsIfNeeded(selections, rest) 130 | } 131 | } 132 | ]; 133 | } 134 | -------------------------------------------------------------------------------- /src/swift/language.ts: -------------------------------------------------------------------------------- 1 | import CodeGenerator from '../utilities/CodeGenerator'; 2 | 3 | import { join, wrap } from '../utilities/printing'; 4 | 5 | export interface Class { 6 | className: string; 7 | modifiers: string[]; 8 | superClass?: string; 9 | adoptedProtocols?: string[]; 10 | } 11 | 12 | export interface Struct { 13 | structName: string; 14 | adoptedProtocols?: string[]; 15 | description?: string; 16 | } 17 | 18 | export interface Protocol { 19 | protocolName: string; 20 | adoptedProtocols?: string[]; 21 | } 22 | 23 | export interface Property { 24 | propertyName: string; 25 | typeName: string; 26 | isOptional?: boolean; 27 | description?: string; 28 | } 29 | 30 | export function escapedString(string: string) { 31 | return string.replace(/"/g, '\\"').replace(/\n/g, '\\n'); 32 | } 33 | 34 | // prettier-ignore 35 | const reservedKeywords = new Set(['associatedtype', 'class', 'deinit', 'enum', 'extension', 36 | 'fileprivate', 'func', 'import', 'init', 'inout', 'internal', 'let', 'open', 37 | 'operator', 'private', 'protocol', 'public', 'static', 'struct', 'subscript', 38 | 'typealias', 'var', 'break', 'case', 'continue', 'default', 'defer', 'do', 39 | 'else', 'fallthrough', 'for', 'guard', 'if', 'in', 'repeat', 'return', 40 | 'switch', 'where', 'while', 'as', 'Any', 'catch', 'false', 'is', 'nil', 41 | 'rethrows', 'super', 'self', 'Self', 'throw', 'throws', 'true', 'try', 42 | 'associativity', 'convenience', 'dynamic', 'didSet', 'final', 'get', 'infix', 43 | 'indirect', 'lazy', 'left', 'mutating', 'none', 'nonmutating', 'optional', 44 | 'override', 'postfix', 'precedence', 'prefix', 'Protocol', 'required', 'right', 45 | 'set', 'Type', 'unowned', 'weak', 'willSet']); 46 | 47 | export function escapeIdentifierIfNeeded(identifier: string) { 48 | if (reservedKeywords.has(identifier)) { 49 | return '`' + identifier + '`'; 50 | } else { 51 | return identifier; 52 | } 53 | } 54 | 55 | export class SwiftGenerator extends CodeGenerator { 56 | constructor(context: Context) { 57 | super(context); 58 | } 59 | 60 | multilineString(string: string) { 61 | this.printOnNewline(`"${escapedString(string)}"`); 62 | } 63 | 64 | comment(comment?: string) { 65 | comment && 66 | comment.split('\n').forEach(line => { 67 | this.printOnNewline(`/// ${line.trim()}`); 68 | }); 69 | } 70 | 71 | deprecationAttributes(isDeprecated: boolean | undefined, deprecationReason: string | undefined) { 72 | if (isDeprecated !== undefined && isDeprecated) { 73 | deprecationReason = (deprecationReason !== undefined && deprecationReason.length > 0) ? deprecationReason : "" 74 | this.printOnNewline(`@available(*, deprecated, message: "${escapedString(deprecationReason)}")`) 75 | } 76 | } 77 | 78 | namespaceDeclaration(namespace: string | undefined, closure: Function) { 79 | if (namespace) { 80 | this.printNewlineIfNeeded(); 81 | this.printOnNewline(`/// ${namespace} namespace`); 82 | this.printOnNewline(`public enum ${namespace}`); 83 | this.pushScope({ typeName: namespace }); 84 | this.withinBlock(closure); 85 | this.popScope(); 86 | } else { 87 | if (closure) { 88 | closure(); 89 | } 90 | } 91 | } 92 | 93 | namespaceExtensionDeclaration(namespace: string | undefined, closure: Function) { 94 | if (namespace) { 95 | this.printNewlineIfNeeded(); 96 | this.printOnNewline(`/// ${namespace} namespace`); 97 | this.printOnNewline(`public extension ${namespace}`); 98 | this.pushScope({ typeName: namespace }); 99 | this.withinBlock(closure); 100 | this.popScope(); 101 | } else { 102 | if (closure) { 103 | closure(); 104 | } 105 | } 106 | } 107 | 108 | classDeclaration({ className, modifiers, superClass, adoptedProtocols = [] }: Class, closure: Function) { 109 | this.printNewlineIfNeeded(); 110 | this.printOnNewline(wrap('', join(modifiers, ' '), ' ') + `class ${className}`); 111 | this.print(wrap(': ', join([superClass, ...adoptedProtocols], ', '))); 112 | this.pushScope({ typeName: className }); 113 | this.withinBlock(closure); 114 | this.popScope(); 115 | } 116 | 117 | structDeclaration({ structName, description, adoptedProtocols = [] }: Struct, closure: Function) { 118 | this.printNewlineIfNeeded(); 119 | this.comment(description); 120 | this.printOnNewline(`public struct ${escapeIdentifierIfNeeded(structName)}`); 121 | this.print(wrap(': ', join(adoptedProtocols, ', '))); 122 | this.pushScope({ typeName: structName }); 123 | this.withinBlock(closure); 124 | this.popScope(); 125 | } 126 | 127 | propertyDeclaration({ propertyName, typeName, description }: Property) { 128 | this.comment(description); 129 | this.printOnNewline(`public var ${escapeIdentifierIfNeeded(propertyName)}: ${typeName}`); 130 | } 131 | 132 | propertyDeclarations(properties: Property[]) { 133 | if (!properties) return; 134 | properties.forEach(property => this.propertyDeclaration(property)); 135 | } 136 | 137 | protocolDeclaration({ protocolName, adoptedProtocols }: Protocol, closure: Function) { 138 | this.printNewlineIfNeeded(); 139 | this.printOnNewline(`public protocol ${protocolName}`); 140 | this.print(wrap(': ', join(adoptedProtocols, ', '))); 141 | this.pushScope({ typeName: protocolName }); 142 | this.withinBlock(closure); 143 | this.popScope(); 144 | } 145 | 146 | protocolPropertyDeclaration({ propertyName, typeName }: Property) { 147 | this.printOnNewline(`var ${propertyName}: ${typeName} { get }`); 148 | } 149 | 150 | protocolPropertyDeclarations(properties: Property[]) { 151 | if (!properties) return; 152 | properties.forEach(property => this.protocolPropertyDeclaration(property)); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/utilities/graphql.ts: -------------------------------------------------------------------------------- 1 | import { 2 | visit, 3 | Kind, 4 | isEqualType, 5 | isAbstractType, 6 | SchemaMetaFieldDef, 7 | TypeMetaFieldDef, 8 | TypeNameMetaFieldDef, 9 | GraphQLCompositeType, 10 | GraphQLObjectType, 11 | GraphQLInterfaceType, 12 | GraphQLUnionType, 13 | GraphQLString, 14 | GraphQLInt, 15 | GraphQLFloat, 16 | GraphQLBoolean, 17 | GraphQLID, 18 | GraphQLError, 19 | GraphQLSchema, 20 | GraphQLType, 21 | GraphQLScalarType, 22 | ASTNode, 23 | Location, 24 | ValueNode, 25 | OperationDefinitionNode, 26 | SelectionSetNode, 27 | FieldNode, 28 | GraphQLField, 29 | GraphQLList, 30 | GraphQLNonNull 31 | } from 'graphql'; 32 | 33 | export function isList(type: GraphQLType): boolean { 34 | return type instanceof GraphQLList || (type instanceof GraphQLNonNull && type.ofType instanceof GraphQLList); 35 | } 36 | 37 | const builtInScalarTypes = new Set([GraphQLString, GraphQLInt, GraphQLFloat, GraphQLBoolean, GraphQLID]); 38 | 39 | export function isBuiltInScalarType(type: GraphQLScalarType) { 40 | return builtInScalarTypes.has(type); 41 | } 42 | 43 | export function isMetaFieldName(name: string) { 44 | return name.startsWith('__'); 45 | } 46 | 47 | const typenameField = { kind: Kind.FIELD, name: { kind: Kind.NAME, value: '__typename' } }; 48 | 49 | export function withTypenameFieldAddedWhereNeeded(ast: ASTNode) { 50 | return visit(ast, { 51 | enter: { 52 | SelectionSet(node: SelectionSetNode) { 53 | return { 54 | ...node, 55 | selections: node.selections.filter( 56 | selection => !(selection.kind === 'Field' && (selection as FieldNode).name.value === '__typename') 57 | ) 58 | }; 59 | } 60 | }, 61 | leave(node: ASTNode) { 62 | if (!(node.kind === 'Field' || node.kind === 'FragmentDefinition')) return undefined; 63 | if (!node.selectionSet) return undefined; 64 | 65 | if (true) { 66 | return { 67 | ...node, 68 | selectionSet: { 69 | ...node.selectionSet, 70 | selections: [typenameField, ...node.selectionSet.selections] 71 | } 72 | }; 73 | } else { 74 | return undefined; 75 | } 76 | } 77 | }); 78 | } 79 | 80 | export function sourceAt(location: Location) { 81 | return location.source.body.slice(location.start, location.end); 82 | } 83 | 84 | export function filePathForNode(node: ASTNode): string { 85 | const name = node.loc && node.loc.source && node.loc.source.name; 86 | if (!name || name === "GraphQL") { 87 | throw new Error("Node does not seem to have a file path"); 88 | } 89 | return name; 90 | } 91 | 92 | export function valueFromValueNode(valueNode: ValueNode): any | { kind: 'Variable', variableName: string } { 93 | switch (valueNode.kind) { 94 | case 'IntValue': 95 | case 'FloatValue': 96 | return Number(valueNode.value); 97 | case 'NullValue': 98 | return null; 99 | case 'ListValue': 100 | return valueNode.values.map(valueFromValueNode); 101 | case 'ObjectValue': 102 | return valueNode.fields.reduce((object, field) => { 103 | object[field.name.value] = valueFromValueNode(field.value); 104 | return object; 105 | }, {} as any); 106 | case 'Variable': 107 | return { kind: 'Variable', variableName: valueNode.name.value }; 108 | default: 109 | return valueNode.value; 110 | } 111 | } 112 | 113 | export function isTypeProperSuperTypeOf(schema: GraphQLSchema, maybeSuperType: GraphQLCompositeType, subType: GraphQLCompositeType) { 114 | return isEqualType(maybeSuperType, subType) || subType instanceof GraphQLObjectType && (isAbstractType(maybeSuperType) && schema.isPossibleType(maybeSuperType, subType)); 115 | } 116 | 117 | // Utility functions extracted from graphql-js 118 | 119 | /** 120 | * Extracts the root type of the operation from the schema. 121 | */ 122 | export function getOperationRootType(schema: GraphQLSchema, operation: OperationDefinitionNode) { 123 | switch (operation.operation) { 124 | case 'query': 125 | return schema.getQueryType(); 126 | case 'mutation': 127 | const mutationType = schema.getMutationType(); 128 | if (!mutationType) { 129 | throw new GraphQLError( 130 | 'Schema is not configured for mutations', 131 | [operation] 132 | ); 133 | } 134 | return mutationType; 135 | case 'subscription': 136 | const subscriptionType = schema.getSubscriptionType(); 137 | if (!subscriptionType) { 138 | throw new GraphQLError( 139 | 'Schema is not configured for subscriptions', 140 | [operation] 141 | ); 142 | } 143 | return subscriptionType; 144 | default: 145 | throw new GraphQLError( 146 | 'Can only compile queries, mutations and subscriptions', 147 | [operation] 148 | ); 149 | } 150 | } 151 | 152 | /** 153 | * Not exactly the same as the executor's definition of getFieldDef, in this 154 | * statically evaluated environment we do not always have an Object type, 155 | * and need to handle Interface and Union types. 156 | */ 157 | export function getFieldDef(schema: GraphQLSchema, parentType: GraphQLCompositeType, fieldAST: FieldNode): GraphQLField | undefined { 158 | const name = fieldAST.name.value; 159 | if (name === SchemaMetaFieldDef.name && 160 | schema.getQueryType() === parentType) { 161 | return SchemaMetaFieldDef; 162 | } 163 | if (name === TypeMetaFieldDef.name && 164 | schema.getQueryType() === parentType) { 165 | return TypeMetaFieldDef; 166 | } 167 | if (name === TypeNameMetaFieldDef.name && 168 | (parentType instanceof GraphQLObjectType || 169 | parentType instanceof GraphQLInterfaceType || 170 | parentType instanceof GraphQLUnionType) 171 | ) { 172 | return TypeNameMetaFieldDef; 173 | } 174 | if (parentType instanceof GraphQLObjectType || 175 | parentType instanceof GraphQLInterfaceType) { 176 | return parentType.getFields()[name]; 177 | } 178 | 179 | return undefined; 180 | } 181 | -------------------------------------------------------------------------------- /src/flow-modern/__tests__/codeGeneration.ts: -------------------------------------------------------------------------------- 1 | import { parse } from 'graphql'; 2 | 3 | import { loadSchema } from '../../loading'; 4 | const schema = loadSchema(require.resolve('../../../test/fixtures/starwars/schema.json')); 5 | 6 | import { 7 | compileToIR, 8 | CompilerOptions, 9 | CompilerContext, 10 | } from '../../compiler'; 11 | 12 | import { generateSource } from '../codeGeneration'; 13 | 14 | function compile( 15 | source: string, 16 | options: CompilerOptions = { 17 | mergeInFieldsFromFragmentSpreads: true, 18 | addTypename: true 19 | } 20 | ): CompilerContext { 21 | const document = parse(source); 22 | return compileToIR(schema, document, options); 23 | } 24 | 25 | describe('Flow codeGeneration', () => { 26 | test('multiple files', () => { 27 | const context = compile(` 28 | query HeroName($episode: Episode) { 29 | hero(episode: $episode) { 30 | name 31 | id 32 | } 33 | } 34 | 35 | query SomeOther($episode: Episode) { 36 | hero(episode: $episode) { 37 | name 38 | ...someFragment 39 | } 40 | } 41 | 42 | fragment someFragment on Character { 43 | appearsIn 44 | } 45 | 46 | mutation ReviewMovie($episode: Episode, $review: ReviewInput) { 47 | createReview(episode: $episode, review: $review) { 48 | stars 49 | commentary 50 | } 51 | } 52 | `); 53 | context.operations["HeroName"].filePath = '/some/file/ComponentA.js'; 54 | context.operations["SomeOther"].filePath = '/some/file/ComponentB.js'; 55 | context.fragments['someFragment'].filePath = '/some/file/ComponentB.js'; 56 | const output = generateSource(context); 57 | expect(output).toBeInstanceOf(Object); 58 | Object.keys(output) 59 | .forEach((filePath) => { 60 | expect(filePath).toMatchSnapshot(); 61 | expect(output[filePath]).toMatchSnapshot(); 62 | }); 63 | }); 64 | 65 | test('simple hero query', () => { 66 | const context = compile(` 67 | query HeroName($episode: Episode) { 68 | hero(episode: $episode) { 69 | name 70 | id 71 | } 72 | } 73 | `); 74 | 75 | const output = generateSource(context); 76 | expect(output).toMatchSnapshot(); 77 | }); 78 | 79 | test('simple mutation', () => { 80 | const context = compile(` 81 | mutation ReviewMovie($episode: Episode, $review: ReviewInput) { 82 | createReview(episode: $episode, review: $review) { 83 | stars 84 | commentary 85 | } 86 | } 87 | `); 88 | 89 | const output = generateSource(context); 90 | expect(output).toMatchSnapshot(); 91 | }); 92 | 93 | test('simple fragment', () => { 94 | const context = compile(` 95 | fragment SimpleFragment on Character{ 96 | name 97 | } 98 | `); 99 | 100 | const output = generateSource(context); 101 | expect(output).toMatchSnapshot(); 102 | }); 103 | 104 | test('fragment with fragment spreads', () => { 105 | const context = compile(` 106 | fragment simpleFragment on Character { 107 | name 108 | } 109 | 110 | fragment anotherFragment on Character { 111 | id 112 | ...simpleFragment 113 | } 114 | `); 115 | 116 | const output = generateSource(context); 117 | expect(output).toMatchSnapshot(); 118 | }); 119 | 120 | test('fragment with fragment spreads with inline fragment', () => { 121 | const context = compile(` 122 | fragment simpleFragment on Character { 123 | name 124 | } 125 | 126 | fragment anotherFragment on Character { 127 | id 128 | ...simpleFragment 129 | 130 | ... on Human { 131 | appearsIn 132 | } 133 | } 134 | `); 135 | 136 | const output = generateSource(context); 137 | expect(output).toMatchSnapshot(); 138 | }); 139 | 140 | test('query with fragment spreads', () => { 141 | const context = compile(` 142 | fragment simpleFragment on Character { 143 | name 144 | } 145 | 146 | query HeroFragment($episode: Episode) { 147 | hero(episode: $episode) { 148 | ...simpleFragment 149 | id 150 | } 151 | } 152 | `); 153 | 154 | const output = generateSource(context); 155 | expect(output).toMatchSnapshot(); 156 | }); 157 | 158 | test('inline fragment', () => { 159 | const context = compile(` 160 | query HeroInlineFragment($episode: Episode) { 161 | hero(episode: $episode) { 162 | ... on Character { 163 | name 164 | } 165 | id 166 | } 167 | } 168 | `); 169 | 170 | const output = generateSource(context); 171 | expect(output).toMatchSnapshot(); 172 | }) 173 | 174 | test('inline fragment on type conditions', () => { 175 | const context = compile(` 176 | query HeroName($episode: Episode) { 177 | hero(episode: $episode) { 178 | name 179 | id 180 | 181 | ... on Human { 182 | homePlanet 183 | friends { 184 | name 185 | } 186 | } 187 | 188 | ... on Droid { 189 | appearsIn 190 | } 191 | } 192 | } 193 | `); 194 | const output = generateSource(context); 195 | expect(output).toMatchSnapshot(); 196 | }); 197 | 198 | test('inline fragment on type conditions with differing inner fields', () => { 199 | const context = compile(` 200 | query HeroName($episode: Episode) { 201 | hero(episode: $episode) { 202 | name 203 | id 204 | 205 | ... on Human { 206 | homePlanet 207 | friends { 208 | name 209 | } 210 | } 211 | 212 | ... on Droid { 213 | appearsIn 214 | friends { 215 | id 216 | } 217 | } 218 | } 219 | } 220 | `); 221 | 222 | const output = generateSource(context); 223 | expect(output).toMatchSnapshot(); 224 | }); 225 | 226 | test('fragment spreads with inline fragments', () => { 227 | const context = compile(` 228 | query HeroName($episode: Episode) { 229 | hero(episode: $episode) { 230 | name 231 | id 232 | ...humanFragment 233 | ...droidFragment 234 | } 235 | } 236 | 237 | fragment humanFragment on Human { 238 | homePlanet 239 | friends { 240 | ... on Human { 241 | name 242 | } 243 | 244 | ... on Droid { 245 | id 246 | } 247 | } 248 | } 249 | 250 | fragment droidFragment on Droid { 251 | appearsIn 252 | } 253 | `); 254 | const output = generateSource(context); 255 | expect(output).toMatchSnapshot(); 256 | }); 257 | }); 258 | -------------------------------------------------------------------------------- /src/compiler/visitors/typeCase.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from 'util'; 2 | 3 | import { GraphQLObjectType } from 'graphql'; 4 | 5 | import { SelectionSet, Selection, Field, FragmentSpread } from '../'; 6 | import { collectAndMergeFields } from './collectAndMergeFields'; 7 | 8 | export class Variant implements SelectionSet { 9 | constructor( 10 | public possibleTypes: GraphQLObjectType[], 11 | public selections: Selection[] = [], 12 | public fragmentSpreads: FragmentSpread[] = [] 13 | ) {} 14 | 15 | get fields(): Field[] { 16 | return collectAndMergeFields(this); 17 | } 18 | 19 | inspect() { 20 | return `${inspect(this.possibleTypes)} -> ${inspect( 21 | collectAndMergeFields(this, false).map(field => field.responseKey) 22 | )} ${inspect(this.fragmentSpreads.map(fragmentSpread => fragmentSpread.fragmentName))}\n`; 23 | } 24 | } 25 | 26 | export function typeCaseForSelectionSet( 27 | selectionSet: SelectionSet, 28 | mergeInFragmentSpreads: boolean = true 29 | ): TypeCase { 30 | const typeCase = new TypeCase(selectionSet.possibleTypes); 31 | 32 | for (const selection of selectionSet.selections) { 33 | switch (selection.kind) { 34 | case 'Field': 35 | for (const variant of typeCase.disjointVariantsFor(selectionSet.possibleTypes)) { 36 | variant.selections.push(selection); 37 | } 38 | break; 39 | case 'FragmentSpread': 40 | if ( 41 | typeCase.default.fragmentSpreads.some( 42 | fragmentSpread => fragmentSpread.fragmentName === selection.fragmentName 43 | ) 44 | ) 45 | continue; 46 | 47 | for (const variant of typeCase.disjointVariantsFor(selectionSet.possibleTypes)) { 48 | variant.fragmentSpreads.push(selection); 49 | 50 | if (!mergeInFragmentSpreads) { 51 | variant.selections.push(selection); 52 | } 53 | } 54 | if (mergeInFragmentSpreads) { 55 | typeCase.merge( 56 | typeCaseForSelectionSet( 57 | { 58 | possibleTypes: selectionSet.possibleTypes.filter(type => 59 | selection.selectionSet.possibleTypes.includes(type) 60 | ), 61 | selections: selection.selectionSet.selections 62 | }, 63 | mergeInFragmentSpreads 64 | ) 65 | ); 66 | } 67 | break; 68 | case 'TypeCondition': 69 | typeCase.merge( 70 | typeCaseForSelectionSet( 71 | { 72 | possibleTypes: selectionSet.possibleTypes.filter(type => 73 | selection.selectionSet.possibleTypes.includes(type) 74 | ), 75 | selections: selection.selectionSet.selections 76 | }, 77 | mergeInFragmentSpreads 78 | ) 79 | ); 80 | break; 81 | case 'BooleanCondition': 82 | typeCase.merge( 83 | typeCaseForSelectionSet(selection.selectionSet, mergeInFragmentSpreads), 84 | selectionSet => [ 85 | { 86 | ...selection, 87 | selectionSet 88 | } 89 | ] 90 | ); 91 | break; 92 | } 93 | } 94 | 95 | return typeCase; 96 | } 97 | 98 | export class TypeCase { 99 | default: Variant; 100 | private variantsByType: Map; 101 | 102 | get variants(): Variant[] { 103 | // Unique the variants before returning them. 104 | return Array.from(new Set(this.variantsByType.values())); 105 | } 106 | 107 | get defaultAndVariants(): Variant[] { 108 | return [this.default, ...this.variants]; 109 | } 110 | 111 | get remainder(): Variant | undefined { 112 | if (this.default.possibleTypes.some(type => !this.variantsByType.has(type))) { 113 | return new Variant( 114 | this.default.possibleTypes.filter(type => !this.variantsByType.has(type)), 115 | this.default.selections, 116 | this.default.fragmentSpreads 117 | ); 118 | } else { 119 | return undefined; 120 | } 121 | } 122 | 123 | get exhaustiveVariants(): Variant[] { 124 | const remainder = this.remainder; 125 | if (remainder) { 126 | return [remainder, ...this.variants]; 127 | } else { 128 | return this.variants; 129 | } 130 | } 131 | 132 | constructor(possibleTypes: GraphQLObjectType[]) { 133 | // We start out with a single default variant that represents all possible types of the selection set. 134 | this.default = new Variant(possibleTypes); 135 | 136 | this.variantsByType = new Map(); 137 | } 138 | 139 | // Returns records representing a set of possible types, making sure they are disjoint with other possible types. 140 | // That may involve refining the existing partition (https://en.wikipedia.org/wiki/Partition_refinement) 141 | // with the passed in set of possible types. 142 | disjointVariantsFor(possibleTypes: GraphQLObjectType[]): Variant[] { 143 | const variants: Variant[] = []; 144 | 145 | const matchesDefault = this.default.possibleTypes.every(type => possibleTypes.includes(type)); 146 | 147 | if (matchesDefault) { 148 | variants.push(this.default); 149 | } 150 | 151 | // We keep a map from original records to split records. We'll then remove possible types from the 152 | // original record and move them to the split record. 153 | // This means the original record will be modified to represent the set theoretical difference between 154 | // the original set of possible types and the refinement set, and the split record will represent the 155 | // intersection. 156 | const splits: Map = new Map(); 157 | 158 | for (const type of possibleTypes) { 159 | let original = this.variantsByType.get(type); 160 | 161 | if (!original) { 162 | if (matchesDefault) continue; 163 | original = this.default; 164 | } 165 | 166 | let split = splits.get(original); 167 | if (!split) { 168 | split = new Variant([], [...original.selections], [...original.fragmentSpreads]); 169 | splits.set(original, split); 170 | variants.push(split); 171 | } 172 | 173 | if (original !== this.default) { 174 | original.possibleTypes.splice(original.possibleTypes.indexOf(type), 1); 175 | } 176 | 177 | this.variantsByType.set(type, split); 178 | split.possibleTypes.push(type); 179 | } 180 | 181 | return variants; 182 | } 183 | 184 | merge(otherTypeCase: TypeCase, transform?: (selectionSet: SelectionSet) => Selection[]) { 185 | for (const otherVariant of otherTypeCase.defaultAndVariants) { 186 | if (otherVariant.selections.length < 1) continue; 187 | for (const variant of this.disjointVariantsFor(otherVariant.possibleTypes)) { 188 | if (otherVariant.fragmentSpreads.length > 0) { 189 | // Union of variant.fragmentSpreads and otherVariant.fragmentSpreads 190 | variant.fragmentSpreads = [...variant.fragmentSpreads, ...otherVariant.fragmentSpreads].filter( 191 | (a, index, array) => array.findIndex(b => b.fragmentName == a.fragmentName) == index 192 | ); 193 | } 194 | variant.selections.push(...(transform ? transform(otherVariant) : otherVariant.selections)); 195 | } 196 | } 197 | } 198 | 199 | inspect() { 200 | return ( 201 | `TypeCase\n` + 202 | ` default -> ${inspect(this.default)}\n` + 203 | this.variants.map(variant => ` ${inspect(variant)}\n`).join('') 204 | ); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as glob from 'glob'; 4 | import * as process from 'process'; 5 | import * as path from 'path'; 6 | import * as yargs from 'yargs'; 7 | 8 | import { downloadSchema, introspectSchema, printSchema, generate } from '.'; 9 | import { ToolError, logError } from './errors' 10 | 11 | import 'source-map-support/register' 12 | 13 | // Make sure unhandled errors in async code are propagated correctly 14 | process.on('unhandledRejection', (error) => { throw error }); 15 | 16 | process.on('uncaughtException', handleError); 17 | 18 | function handleError(error) { 19 | logError(error); 20 | process.exit(1); 21 | } 22 | 23 | yargs 24 | .command( 25 | ['introspect-schema ', 'download-schema'], 26 | 'Generate an introspection JSON from a local GraphQL file or from a remote GraphQL server', 27 | { 28 | output: { 29 | demand: true, 30 | describe: 'Output path for GraphQL schema file', 31 | default: 'schema.json', 32 | normalize: true, 33 | coerce: path.resolve, 34 | }, 35 | header: { 36 | alias: 'H', 37 | describe: 'Additional header to send to the server as part of the introspection query request', 38 | type: 'array', 39 | coerce: (arg) => { 40 | let additionalHeaders = {}; 41 | for (const header of arg) { 42 | const [name, value] = header.split(/\s*:\s*/); 43 | if (!(name && value)) { 44 | throw new ToolError('Headers should be specified as "Name: Value"'); 45 | } 46 | additionalHeaders[name] = value; 47 | } 48 | return additionalHeaders; 49 | } 50 | }, 51 | insecure: { 52 | alias: 'K', 53 | describe: 'Allows "insecure" SSL connection to the server', 54 | type: 'boolean' 55 | }, 56 | method: { 57 | demand: false, 58 | describe: 'The HTTP request method to use for the introspection query request', 59 | type: 'string', 60 | default: 'POST', 61 | choices: ['POST', 'GET', 'post', 'get'] 62 | } 63 | }, 64 | async argv => { 65 | const { schema, output, header, insecure, method } = argv; 66 | 67 | const urlRegex = /^https?:\/\//i; 68 | if (urlRegex.test(schema)) { 69 | await downloadSchema(schema, output, header, insecure, method); 70 | } else { 71 | await introspectSchema(schema, output); 72 | } 73 | } 74 | ) 75 | .command( 76 | ['print-schema [schema]'], 77 | 'Print the provided schema in the GraphQL schema language format', 78 | { 79 | schema: { 80 | demand: true, 81 | describe: 'Path to GraphQL introspection query result', 82 | default: 'schema.json', 83 | normalize: true, 84 | coerce: path.resolve, 85 | }, 86 | output: { 87 | demand: true, 88 | describe: 'Output path for GraphQL schema language file', 89 | default: 'schema.graphql', 90 | normalize: true, 91 | coerce: path.resolve, 92 | } 93 | }, 94 | async argv => { 95 | const { schema, output } = argv; 96 | await printSchema(schema, output); 97 | } 98 | ) 99 | .command( 100 | 'generate [input...]', 101 | 'Generate code from a GraphQL schema and query documents', 102 | { 103 | schema: { 104 | demand: false, 105 | describe: 'Path to GraphQL schema file. (Defaults to using .graphqlconfig or schema.json)', 106 | normalize: true, 107 | coerce: path.resolve, 108 | }, 109 | output: { 110 | describe: 'Output directory for the generated files', 111 | normalize: true, 112 | coerce: path.resolve, 113 | }, 114 | target: { 115 | demand: false, 116 | describe: 'Code generation target language', 117 | choices: ['swift', 'scala', 'json', 'ts', 'typescript', 'flow', 'flow-modern'], 118 | default: 'swift' 119 | }, 120 | only: { 121 | describe: 'Parse all input files, but only output generated code for the specified file [Swift only]', 122 | normalize: true, 123 | coerce: path.resolve, 124 | }, 125 | namespace: { 126 | demand: false, 127 | describe: 'Optional namespace for generated types [currently Swift and Scala-only]', 128 | type: 'string' 129 | }, 130 | "passthrough-custom-scalars": { 131 | demand: false, 132 | describe: "Don't attempt to map custom scalars [temporary option]", 133 | default: false 134 | }, 135 | "custom-scalars-prefix": { 136 | demand: false, 137 | describe: "Prefix for custom scalars. (Implies that passthrough-custom-scalars is true if set)", 138 | default: '', 139 | normalize: true 140 | }, 141 | "add-typename": { 142 | demand: false, 143 | describe: "For non-swift targets, always add the __typename GraphQL introspection type when generating target types", 144 | default: false 145 | }, 146 | "use-flow-exact-objects": { 147 | demand: false, 148 | describe: "Use Flow exact objects for generated types [flow-modern only]", 149 | default: false, 150 | type: 'boolean' 151 | }, 152 | "tag-name": { 153 | demand: false, 154 | describe: "Name of the template literal tag used to identify template literals containing GraphQL queries in Javascript/Typescript code", 155 | default: 'gql' 156 | }, 157 | "project-name": { 158 | demand: false, 159 | describe: "Name of the project to use in a multi-project .graphqlconfig file", 160 | }, 161 | "operation-ids-path": { 162 | demand: false, 163 | describe: "Path to an operation id JSON map file. If specified, also stores the operation ids (hashes) as properties on operation types [currently Swift-only]", 164 | default: null, 165 | normalize: true 166 | }, 167 | "merge-in-fields-from-fragment-spreads": { 168 | demand: false, 169 | describe: "Merge fragment fields onto its enclosing type", 170 | default: true, 171 | type: 'boolean' 172 | }, 173 | "add-s3-wrapper": { 174 | demand: false, 175 | describe: "Adds S3 wrapper code to the output", 176 | default: false 177 | } 178 | }, 179 | argv => { 180 | let { input } = argv; 181 | 182 | // Use glob if the user's shell was unable to expand the pattern 183 | if (input.length === 1 && glob.hasMagic(input[0])) { 184 | input = glob.sync(input[0]); 185 | } 186 | 187 | const inputPaths = input 188 | .map(input => path.resolve(input)) 189 | // Sort to normalize different glob expansions between different terminals. 190 | .sort(); 191 | 192 | const options = { 193 | passthroughCustomScalars: argv["passthrough-custom-scalars"] || argv["custom-scalars-prefix"] !== '', 194 | customScalarsPrefix: argv["custom-scalars-prefix"] || '', 195 | addTypename: argv["add-typename"], 196 | namespace: argv.namespace, 197 | operationIdsPath: argv["operation-ids-path"], 198 | generateOperationIds: !!argv["operation-ids-path"], 199 | mergeInFieldsFromFragmentSpreads: argv["merge-in-fields-from-fragment-spreads"], 200 | useFlowExactObjects: argv['use-flow-exact-objects'], 201 | addS3Wrapper: argv["add-s3-wrapper"] 202 | }; 203 | 204 | generate(inputPaths, argv.schema, argv.output, argv.only, argv.target, argv.tagName, argv.projectName, options); 205 | }, 206 | ) 207 | .fail(function(message, error) { 208 | handleError(error ? error : new ToolError(message)); 209 | }) 210 | .help() 211 | .version() 212 | .strict() 213 | .argv 214 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Apollo Contributor Guide 2 | 3 | Excited about Apollo and want to make it better? We’re excited too! 4 | 5 | Apollo is a community of developers just like you, striving to create the best tools and libraries around GraphQL. We welcome anyone who wants to contribute or provide constructive feedback, no matter the age or level of experience. If you want to help but don't know where to start, let us know, and we'll find something for you. 6 | 7 | Oh, and if you haven't already, sign up for the [Apollo Slack](http://www.apollodata.com/#slack). 8 | 9 | Here are some ways to contribute to the project, from easiest to most difficult: 10 | 11 | * [Reporting bugs](#reporting-bugs) 12 | * [Improving the documentation](#improving-the-documentation) 13 | * [Responding to issues](#responding-to-issues) 14 | * [Small bug fixes](#small-bug-fixes) 15 | * [Suggesting features](#suggesting-features) 16 | * [Big pull requests](#big-prs) 17 | 18 | ## Issues 19 | 20 | ### Reporting bugs 21 | 22 | If you encounter a bug, please file an issue on GitHub via the repository of the sub-project you think contains the bug. If an issue you have is already reported, please add additional information or add a 👍 reaction to indicate your agreement. 23 | 24 | While we will try to be as helpful as we can on any issue reported, please include the following to maximize the chances of a quick fix: 25 | 26 | 1. **Intended outcome:** What you were trying to accomplish when the bug occurred, and as much code as possible related to the source of the problem. 27 | 2. **Actual outcome:** A description of what actually happened, including a screenshot or copy-paste of any related error messages, logs, or other output that might be related. Places to look for information include your browser console, server console, and network logs. Please avoid non-specific phrases like “didn’t work” or “broke”. 28 | 3. **How to reproduce the issue:** Instructions for how the issue can be reproduced by a maintainer or contributor. Be as specific as possible, and only mention what is necessary to reproduce the bug. If possible, try to isolate the exact circumstances in which the bug occurs and avoid speculation over what the cause might be. 29 | 30 | Creating a good reproduction really helps contributors investigate and resolve your issue quickly. In many cases, the act of creating a minimal reproduction illuminates that the source of the bug was somewhere outside the library in question, saving time and effort for everyone. 31 | 32 | ### Improving the documentation 33 | 34 | Improving the documentation, examples, and other open source content can be the easiest way to contribute to the library. If you see a piece of content that can be better, open a PR with an improvement, no matter how small! If you would like to suggest a big change or major rewrite, we’d love to hear your ideas but please open an issue for discussion before writing the PR. 35 | 36 | ### Responding to issues 37 | 38 | In addition to reporting issues, a great way to contribute to Apollo is to respond to other peoples' issues and try to identify the problem or help them work around it. If you’re interested in taking a more active role in this process, please go ahead and respond to issues. And don't forget to say "Hi" on Apollo Slack! 39 | 40 | ### Small bug fixes 41 | 42 | For a small bug fix change (less than 20 lines of code changed), feel free to open a pull request. We’ll try to merge it as fast as possible and ideally publish a new release on the same day. The only requirement is, make sure you also add a test that verifies the bug you are trying to fix. 43 | 44 | ### Suggesting features 45 | 46 | Most of the features in Apollo came from suggestions by you, the community! We welcome any ideas about how to make Apollo better for your use case. Unless there is overwhelming demand for a feature, it might not get implemented immediately, but please include as much information as possible that will help people have a discussion about your proposal: 47 | 48 | 1. **Use case:** What are you trying to accomplish, in specific terms? Often, there might already be a good way to do what you need and a new feature is unnecessary, but it’s hard to know without information about the specific use case. 49 | 2. **Could this be a plugin?** In many cases, a feature might be too niche to be included in the core of a library, and is better implemented as a companion package. If there isn’t a way to extend the library to do what you want, could we add additional plugin APIs? It’s important to make the case for why a feature should be part of the core functionality of the library. 50 | 3. **Is there a workaround?** Is this a more convenient way to do something that is already possible, or is there some blocker that makes a workaround unfeasible? 51 | 52 | Feature requests will be labeled as such, and we encourage using GitHub issues as a place to discuss new features and possible implementation designs. Please refrain from submitting a pull request to implement a proposed feature until there is consensus that it should be included. This way, you can avoid putting in work that can’t be merged in. 53 | 54 | Once there is a consensus on the need for a new feature, proceed as listed below under “Big PRs”. 55 | 56 | ## Big PRs 57 | 58 | This includes: 59 | 60 | - Big bug fixes 61 | - New features 62 | 63 | For significant changes to a repository, it’s important to settle on a design before starting on the implementation. This way, we can make sure that major improvements get the care and attention they deserve. Since big changes can be risky and might not always get merged, it’s good to reduce the amount of possible wasted effort by agreeing on an implementation design/plan first. 64 | 65 | 1. **Open an issue.** Open an issue about your bug or feature, as described above. 66 | 2. **Reach consensus.** Some contributors and community members should reach an agreement that this feature or bug is important, and that someone should work on implementing or fixing it. 67 | 3. **Agree on intended behavior.** On the issue, reach an agreement about the desired behavior. In the case of a bug fix, it should be clear what it means for the bug to be fixed, and in the case of a feature, it should be clear what it will be like for developers to use the new feature. 68 | 4. **Agree on implementation plan.** Write a plan for how this feature or bug fix should be implemented. What modules need to be added or rewritten? Should this be one pull request or multiple incremental improvements? Who is going to do each part? 69 | 5. **Submit PR.** In the case where multiple dependent patches need to be made to implement the change, only submit one at a time. Otherwise, the others might get stale while the first is reviewed and merged. Make sure to avoid “while we’re here” type changes - if something isn’t relevant to the improvement at hand, it should be in a separate PR; this especially includes code style changes of unrelated code. 70 | 6. **Review.** At least one core contributor should sign off on the change before it’s merged. Look at the “code review” section below to learn about factors are important in the code review. If you want to expedite the code being merged, try to review your own code first! 71 | 7. **Merge and release!** 72 | 73 | ### Code review guidelines 74 | 75 | It’s important that every piece of code in Apollo packages is reviewed by at least one core contributor familiar with that codebase. Here are some things we look for: 76 | 77 | 1. **Required CI checks pass.** This is a prerequisite for the review, and it is the PR author's responsibility. As long as the tests don’t pass, the PR won't get reviewed. 78 | 2. **Simplicity.** Is this the simplest way to achieve the intended goal? If there are too many files, redundant functions, or complex lines of code, suggest a simpler way to do the same thing. In particular, avoid implementing an overly general solution when a simple, small, and pragmatic fix will do. 79 | 3. **Testing.** Do the tests ensure this code won’t break when other stuff changes around it? When it does break, will the tests added help us identify which part of the library has the problem? Did we cover an appropriate set of edge cases? Look at the test coverage report if there is one. Are all significant code paths in the new code exercised at least once? 80 | 4. **No unnecessary or unrelated changes.** PRs shouldn’t come with random formatting changes, especially in unrelated parts of the code. If there is some refactoring that needs to be done, it should be in a separate PR from a bug fix or feature, if possible. 81 | 5. **Code has appropriate comments.** Code should be commented, or written in a clear “self-documenting” way. 82 | 6. **Idiomatic use of the language.** In TypeScript, make sure the typings are specific and correct. In ES2015, make sure to use imports rather than require and const instead of var, etc. Ideally a linter enforces a lot of this, but use your common sense and follow the style of the surrounding code. 83 | -------------------------------------------------------------------------------- /src/swift/helpers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLType, 3 | GraphQLString, 4 | GraphQLInt, 5 | GraphQLFloat, 6 | GraphQLBoolean, 7 | GraphQLID, 8 | GraphQLList, 9 | GraphQLNonNull, 10 | GraphQLScalarType, 11 | GraphQLEnumType, 12 | isCompositeType, 13 | getNamedType, 14 | GraphQLInputField 15 | } from 'graphql'; 16 | 17 | import { camelCase, pascalCase } from 'change-case'; 18 | import * as Inflector from 'inflected'; 19 | import { join, wrap } from '../utilities/printing'; 20 | 21 | import { Property, Struct } from './language'; 22 | 23 | import { CompilerOptions, SelectionSet, Field, FragmentSpread, Argument } from '../compiler'; 24 | import { isMetaFieldName } from '../utilities/graphql'; 25 | import { Variant } from '../compiler/visitors/typeCase'; 26 | import { collectAndMergeFields } from '../compiler/visitors/collectAndMergeFields'; 27 | import { getTypeForAWSScalar } from './aws-scalar-helper'; 28 | 29 | const builtInScalarMap = { 30 | [GraphQLString.name]: 'String', 31 | [GraphQLInt.name]: 'Int', 32 | [GraphQLFloat.name]: 'Double', 33 | [GraphQLBoolean.name]: 'Bool', 34 | [GraphQLID.name]: 'GraphQLID' 35 | }; 36 | 37 | export class Helpers { 38 | constructor(public options: CompilerOptions) {} 39 | 40 | // Types 41 | 42 | typeNameFromGraphQLType(type: GraphQLType, unmodifiedTypeName?: string, isOptional?: boolean): string { 43 | if (type instanceof GraphQLNonNull) { 44 | return this.typeNameFromGraphQLType(type.ofType, unmodifiedTypeName, false); 45 | } else if (isOptional === undefined) { 46 | isOptional = true; 47 | } 48 | 49 | let typeName; 50 | if (type instanceof GraphQLList) { 51 | typeName = '[' + this.typeNameFromGraphQLType(type.ofType, unmodifiedTypeName) + ']'; 52 | } else if (type instanceof GraphQLScalarType) { 53 | typeName = this.typeNameForScalarType(type); 54 | } else { 55 | typeName = unmodifiedTypeName || type.name; 56 | } 57 | 58 | return isOptional ? typeName + '?' : typeName; 59 | } 60 | 61 | typeNameForScalarType(type: GraphQLScalarType): string { 62 | return ( 63 | builtInScalarMap[type.name] || 64 | (this.options.passthroughCustomScalars 65 | ? this.options.customScalarsPrefix + type.name 66 | : getTypeForAWSScalar(type) ? getTypeForAWSScalar(type): GraphQLString.name) 67 | ); 68 | } 69 | 70 | fieldTypeEnum(type: GraphQLType, structName: string): string { 71 | if (type instanceof GraphQLNonNull) { 72 | return `.nonNull(${this.fieldTypeEnum(type.ofType, structName)})`; 73 | } else if (type instanceof GraphQLList) { 74 | return `.list(${this.fieldTypeEnum(type.ofType, structName)})`; 75 | } else if (type instanceof GraphQLScalarType) { 76 | return `.scalar(${this.typeNameForScalarType(type)}.self)`; 77 | } else if (type instanceof GraphQLEnumType) { 78 | return `.scalar(${type.name}.self)`; 79 | } else if (isCompositeType(type)) { 80 | return `.object(${structName}.selections)`; 81 | } else { 82 | throw new Error(`Unknown field type: ${type}`); 83 | } 84 | } 85 | 86 | // Names 87 | 88 | enumCaseName(name: string) { 89 | return camelCase(name); 90 | } 91 | 92 | enumDotCaseName(name: string) { 93 | return `.${camelCase(name)}`; 94 | } 95 | 96 | operationClassName(name: string) { 97 | return pascalCase(name); 98 | } 99 | 100 | structNameForPropertyName(propertyName: string) { 101 | return pascalCase(Inflector.singularize(propertyName)); 102 | } 103 | 104 | structNameForFragmentName(fragmentName: string) { 105 | return pascalCase(fragmentName); 106 | } 107 | 108 | structNameForVariant(variant: SelectionSet) { 109 | return 'As' + variant.possibleTypes.map(type => pascalCase(type.name)).join('Or'); 110 | } 111 | 112 | // Properties 113 | 114 | propertyFromField(field: Field, namespace?: string): Field & Property & Struct { 115 | const { responseKey, isConditional } = field; 116 | 117 | const propertyName = isMetaFieldName(responseKey) ? responseKey : camelCase(responseKey); 118 | 119 | const structName = join([namespace, this.structNameForPropertyName(responseKey)], '.'); 120 | 121 | let type = field.type; 122 | 123 | if (isConditional && type instanceof GraphQLNonNull) { 124 | type = type.ofType; 125 | } 126 | 127 | const isOptional = !(type instanceof GraphQLNonNull); 128 | 129 | const unmodifiedType = getNamedType(field.type); 130 | 131 | const unmodifiedTypeName = isCompositeType(unmodifiedType) ? structName : unmodifiedType.name; 132 | 133 | const typeName = this.typeNameFromGraphQLType(type, unmodifiedTypeName); 134 | 135 | return Object.assign({}, field, { 136 | responseKey, 137 | propertyName, 138 | typeName, 139 | structName, 140 | isOptional 141 | }); 142 | } 143 | 144 | propertyFromVariant(variant: Variant): Variant & Property & Struct { 145 | const structName = this.structNameForVariant(variant); 146 | 147 | return Object.assign(variant, { 148 | propertyName: camelCase(structName), 149 | typeName: structName + '?', 150 | structName 151 | }); 152 | } 153 | 154 | propertyFromFragmentSpread( 155 | fragmentSpread: FragmentSpread, 156 | isConditional: boolean 157 | ): FragmentSpread & Property & Struct { 158 | const structName = this.structNameForFragmentName(fragmentSpread.fragmentName); 159 | 160 | return Object.assign({}, fragmentSpread, { 161 | propertyName: camelCase(fragmentSpread.fragmentName), 162 | typeName: isConditional ? structName + '?' : structName, 163 | structName, 164 | isConditional 165 | }); 166 | } 167 | 168 | propertyFromInputField(field: GraphQLInputField) { 169 | return Object.assign({}, field, { 170 | propertyName: camelCase(field.name), 171 | typeName: this.typeNameFromGraphQLType(field.type), 172 | isOptional: !(field.type instanceof GraphQLNonNull) 173 | }); 174 | } 175 | 176 | propertiesForSelectionSet( 177 | selectionSet: SelectionSet, 178 | namespace?: string 179 | ): (Field & Property)[] | undefined { 180 | const properties = collectAndMergeFields(selectionSet, true) 181 | .filter(field => field.name !== '__typename') 182 | .map(field => this.propertyFromField(field, namespace)); 183 | 184 | // If we're not merging in fields from fragment spreads, there is no guarantee there will a generated 185 | // type for a composite field, so to avoid compiler errors we skip the initializer for now. 186 | if ( 187 | selectionSet.selections.some(selection => selection.kind === 'FragmentSpread') && 188 | properties.some(property => isCompositeType(getNamedType(property.type))) 189 | ) { 190 | return undefined; 191 | } 192 | 193 | return properties; 194 | } 195 | 196 | // Expressions 197 | 198 | dictionaryLiteralForFieldArguments(args: Argument[]) { 199 | function expressionFromValue(value: any): string { 200 | if (value.kind === 'Variable') { 201 | return `GraphQLVariable("${value.variableName}")`; 202 | } else if (Array.isArray(value)) { 203 | return wrap('[', join(value.map(expressionFromValue), ', '), ']'); 204 | } else if (typeof value === 'object') { 205 | return wrap( 206 | '[', 207 | join( 208 | Object.entries(value).map(([key, value]) => { 209 | return `"${key}": ${expressionFromValue(value)}`; 210 | }), 211 | ', ' 212 | ) || ':', 213 | ']' 214 | ); 215 | } else { 216 | return JSON.stringify(value); 217 | } 218 | } 219 | 220 | return wrap( 221 | '[', 222 | join( 223 | args.map(arg => { 224 | return `"${arg.name}": ${expressionFromValue(arg.value)}`; 225 | }), 226 | ', ' 227 | ) || ':', 228 | ']' 229 | ); 230 | } 231 | 232 | mapExpressionForType( 233 | type: GraphQLType, 234 | expression: (identifier: string) => string, 235 | identifier = '' 236 | ): string { 237 | let isOptional; 238 | if (type instanceof GraphQLNonNull) { 239 | isOptional = false; 240 | type = type.ofType; 241 | } else { 242 | isOptional = true; 243 | } 244 | 245 | if (type instanceof GraphQLList) { 246 | if (isOptional) { 247 | return `${identifier}.flatMap { $0.map { ${this.mapExpressionForType( 248 | type.ofType, 249 | expression, 250 | '$0' 251 | )} } }`; 252 | } else { 253 | return `${identifier}.map { ${this.mapExpressionForType(type.ofType, expression, '$0')} }`; 254 | } 255 | } else if (isOptional) { 256 | return `${identifier}.flatMap { ${expression('$0')} }`; 257 | } else { 258 | return expression(identifier); 259 | } 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/compiler/legacyIR.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema, GraphQLType, GraphQLObjectType, GraphQLCompositeType, DocumentNode } from 'graphql'; 2 | 3 | import { compileToIR, CompilerContext, SelectionSet, Field, FragmentSpread } from './'; 4 | 5 | import { collectFragmentsReferenced } from './visitors/collectFragmentsReferenced'; 6 | import { generateOperationId } from './visitors/generateOperationId'; 7 | import { typeCaseForSelectionSet } from './visitors/typeCase'; 8 | import { collectAndMergeFields } from './visitors/collectAndMergeFields'; 9 | 10 | import '../utilities/array'; 11 | 12 | export interface CompilerOptions { 13 | addTypename?: boolean; 14 | mergeInFieldsFromFragmentSpreads?: boolean; 15 | passthroughCustomScalars?: boolean; 16 | customScalarsPrefix?: string; 17 | namespace?: string; 18 | generateOperationIds?: boolean; 19 | } 20 | 21 | export interface LegacyCompilerContext { 22 | schema: GraphQLSchema; 23 | operations: { [operationName: string]: LegacyOperation }; 24 | fragments: { [fragmentName: string]: LegacyFragment }; 25 | typesUsed: GraphQLType[]; 26 | options: CompilerOptions; 27 | } 28 | 29 | export interface LegacyOperation { 30 | filePath?: string; 31 | operationName: string; 32 | operationId?: string; 33 | operationType: string; 34 | rootType: GraphQLObjectType; 35 | variables: { 36 | name: string; 37 | type: GraphQLType; 38 | }[]; 39 | source: string; 40 | sourceWithFragments?: string; 41 | fields: LegacyField[]; 42 | fragmentSpreads?: string[]; 43 | inlineFragments?: LegacyInlineFragment[]; 44 | fragmentsReferenced: string[]; 45 | } 46 | 47 | export interface LegacyFragment { 48 | filePath?: string; 49 | fragmentName: string; 50 | source: string; 51 | typeCondition: GraphQLCompositeType; 52 | possibleTypes: GraphQLObjectType[]; 53 | fields: LegacyField[]; 54 | fragmentSpreads: string[]; 55 | inlineFragments: LegacyInlineFragment[]; 56 | } 57 | 58 | export interface LegacyInlineFragment { 59 | typeCondition: GraphQLObjectType; 60 | possibleTypes: GraphQLObjectType[]; 61 | fields: LegacyField[]; 62 | fragmentSpreads: string[]; 63 | } 64 | 65 | export interface LegacyField { 66 | responseName: string; 67 | fieldName: string; 68 | args?: Argument[]; 69 | type: GraphQLType; 70 | description?: string; 71 | isConditional?: boolean; 72 | conditions?: BooleanCondition[]; 73 | isDeprecated?: boolean; 74 | deprecationReason?: string; 75 | fields?: LegacyField[]; 76 | fragmentSpreads?: string[]; 77 | inlineFragments?: LegacyInlineFragment[]; 78 | } 79 | 80 | export interface BooleanCondition { 81 | variableName: string; 82 | inverted: boolean; 83 | } 84 | 85 | export interface Argument { 86 | name: string; 87 | value: any; 88 | } 89 | 90 | export function compileToLegacyIR( 91 | schema: GraphQLSchema, 92 | document: DocumentNode, 93 | options: CompilerOptions = { mergeInFieldsFromFragmentSpreads: true } 94 | ): LegacyCompilerContext { 95 | const context = compileToIR(schema, document, options); 96 | const transformer = new LegacyIRTransformer(context, options); 97 | return transformer.transformIR(); 98 | } 99 | 100 | class LegacyIRTransformer { 101 | constructor( 102 | public context: CompilerContext, 103 | public options: CompilerOptions = { mergeInFieldsFromFragmentSpreads: true } 104 | ) {} 105 | 106 | transformIR(): LegacyCompilerContext { 107 | const operations: { [operationName: string]: LegacyOperation } = Object.create({}); 108 | 109 | for (const [operationName, operation] of Object.entries(this.context.operations)) { 110 | const { filePath, operationType, rootType, variables, source, selectionSet } = operation; 111 | const fragmentsReferenced = collectFragmentsReferenced(selectionSet, this.context.fragments); 112 | 113 | const { sourceWithFragments, operationId } = generateOperationId( 114 | operation, 115 | this.context.fragments, 116 | fragmentsReferenced 117 | ); 118 | 119 | operations[operationName] = { 120 | filePath, 121 | operationName, 122 | operationType, 123 | rootType, 124 | variables, 125 | source, 126 | ...this.transformSelectionSetToLegacyIR(selectionSet), 127 | fragmentsReferenced: Array.from(fragmentsReferenced), 128 | sourceWithFragments, 129 | operationId 130 | }; 131 | } 132 | 133 | const fragments: { [fragmentName: string]: LegacyFragment } = Object.create({}); 134 | 135 | for (const [fragmentName, fragment] of Object.entries(this.context.fragments)) { 136 | const { selectionSet, type, ...fragmentWithoutSelectionSet } = fragment; 137 | fragments[fragmentName] = { 138 | typeCondition: type, 139 | possibleTypes: selectionSet.possibleTypes, 140 | ...fragmentWithoutSelectionSet, 141 | ...this.transformSelectionSetToLegacyIR(selectionSet) 142 | }; 143 | } 144 | 145 | const legacyContext: LegacyCompilerContext = { 146 | schema: this.context.schema, 147 | operations, 148 | fragments, 149 | typesUsed: this.context.typesUsed, 150 | options: this.options 151 | }; 152 | 153 | return legacyContext; 154 | } 155 | 156 | transformSelectionSetToLegacyIR(selectionSet: SelectionSet) { 157 | const typeCase = typeCaseForSelectionSet(selectionSet, this.options.mergeInFieldsFromFragmentSpreads); 158 | 159 | const fields: LegacyField[] = this.transformFieldsToLegacyIR(collectAndMergeFields(typeCase.default, false)); 160 | 161 | const inlineFragments: LegacyInlineFragment[] = typeCase.variants.flatMap(variant => { 162 | const fields = this.transformFieldsToLegacyIR(collectAndMergeFields(variant, false)); 163 | 164 | if ( 165 | // Filter out records that represent the same possible types as the default record. 166 | selectionSet.possibleTypes.every(type => variant.possibleTypes.includes(type)) && 167 | // Filter out empty records for consistency with legacy compiler. 168 | fields.length < 1 169 | ) 170 | return undefined; 171 | 172 | const fragmentSpreads: string[] = this.collectFragmentSpreads(selectionSet, variant.possibleTypes).map( 173 | (fragmentSpread: FragmentSpread) => fragmentSpread.fragmentName 174 | ); 175 | return variant.possibleTypes.map(possibleType => { 176 | return { 177 | typeCondition: possibleType, 178 | possibleTypes: [possibleType], 179 | fields, 180 | fragmentSpreads 181 | } as LegacyInlineFragment; 182 | }); 183 | }); 184 | 185 | for (const inlineFragment of inlineFragments) { 186 | inlineFragments[inlineFragment.typeCondition.name as any] = inlineFragment; 187 | } 188 | 189 | const fragmentSpreads: string[] = this.collectFragmentSpreads(selectionSet).map( 190 | (fragmentSpread: FragmentSpread) => fragmentSpread.fragmentName 191 | ); 192 | 193 | return { 194 | fields, 195 | fragmentSpreads, 196 | inlineFragments 197 | }; 198 | } 199 | 200 | transformFieldsToLegacyIR(fields: Field[]) { 201 | return fields.map(field => { 202 | const { args, type, isConditional, description, isDeprecated, deprecationReason, selectionSet } = field; 203 | const conditions = 204 | field.conditions && field.conditions.length > 0 205 | ? field.conditions.map(({ kind, variableName, inverted }) => { 206 | return { 207 | kind, 208 | variableName, 209 | inverted 210 | }; 211 | }) 212 | : undefined; 213 | return { 214 | responseName: field.alias || field.name, 215 | fieldName: field.name, 216 | type, 217 | args, 218 | isConditional, 219 | conditions, 220 | description, 221 | isDeprecated, 222 | deprecationReason, 223 | ...selectionSet ? this.transformSelectionSetToLegacyIR(selectionSet) : {} 224 | } as LegacyField; 225 | }); 226 | } 227 | 228 | collectFragmentSpreads( 229 | selectionSet: SelectionSet, 230 | possibleTypes: GraphQLObjectType[] = selectionSet.possibleTypes 231 | ): FragmentSpread[] { 232 | const fragmentSpreads: FragmentSpread[] = []; 233 | 234 | for (const selection of selectionSet.selections) { 235 | switch (selection.kind) { 236 | case 'FragmentSpread': 237 | fragmentSpreads.push(selection); 238 | break; 239 | case 'TypeCondition': 240 | if (possibleTypes.every(type => selection.selectionSet.possibleTypes.includes(type))) { 241 | fragmentSpreads.push(...this.collectFragmentSpreads(selection.selectionSet, possibleTypes)); 242 | } 243 | break; 244 | case 'BooleanCondition': 245 | fragmentSpreads.push(...this.collectFragmentSpreads(selection.selectionSet, possibleTypes)); 246 | break; 247 | } 248 | } 249 | 250 | // Unique the fragment spreads before returning them. 251 | return Array.from(new Set(fragmentSpreads)); 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /test/typescript/codeGeneration.js: -------------------------------------------------------------------------------- 1 | import { stripIndent } from 'common-tags'; 2 | 3 | import { 4 | parse, 5 | isType, 6 | GraphQLID, 7 | GraphQLString, 8 | GraphQLInt, 9 | GraphQLList, 10 | GraphQLNonNull 11 | } from 'graphql'; 12 | 13 | import { 14 | generateSource 15 | } from '../../src/typescript/codeGeneration'; 16 | 17 | import { loadSchema } from '../../src/loading'; 18 | const starWarsSchema = loadSchema(require.resolve('../fixtures/starwars/schema.json')); 19 | const miscSchema = loadSchema(require.resolve('../fixtures/misc/schema.json')); 20 | 21 | import CodeGenerator from '../../src/utilities/CodeGenerator'; 22 | 23 | import { compileToLegacyIR } from '../../src/compiler/legacyIR'; 24 | 25 | describe('TypeScript code generation', function() { 26 | let generator; 27 | let compileFromSource; 28 | let addFragment; 29 | 30 | function setup(schema) { 31 | const context = { 32 | schema: schema, 33 | operations: {}, 34 | fragments: {}, 35 | typesUsed: {} 36 | } 37 | 38 | generator = new CodeGenerator(context); 39 | 40 | compileFromSource = (source) => { 41 | const document = parse(source); 42 | const context = compileToLegacyIR(schema, document, { 43 | mergeInFieldsFromFragmentSpreads: true, 44 | addTypename: true 45 | }); 46 | generator.context = context; 47 | return context; 48 | }; 49 | 50 | addFragment = (fragment) => { 51 | generator.context.fragments[fragment.fragmentName] = fragment; 52 | }; 53 | 54 | return { generator, compileFromSource, addFragment }; 55 | } 56 | 57 | describe('#generateSource()', function() { 58 | test(`should generate simple query operations`, function() { 59 | const { compileFromSource } = setup(starWarsSchema); 60 | const context = compileFromSource(` 61 | query HeroName { 62 | hero { 63 | name 64 | } 65 | } 66 | `); 67 | 68 | const source = generateSource(context); 69 | expect(source).toMatchSnapshot(); 70 | }); 71 | 72 | test(`should generate simple query operations including input variables`, function() { 73 | const { compileFromSource } = setup(starWarsSchema); 74 | const context = compileFromSource(` 75 | query HeroName($episode: Episode) { 76 | hero(episode: $episode) { 77 | name 78 | } 79 | } 80 | `); 81 | 82 | const source = generateSource(context); 83 | expect(source).toMatchSnapshot(); 84 | }); 85 | 86 | test(`should generate simple nested query operations including input variables`, function() { 87 | const { compileFromSource } = setup(starWarsSchema); 88 | const context = compileFromSource(` 89 | query HeroAndFriendsNames($episode: Episode) { 90 | hero(episode: $episode) { 91 | name 92 | friends { 93 | name 94 | } 95 | } 96 | } 97 | `); 98 | 99 | const source = generateSource(context); 100 | expect(source).toMatchSnapshot(); 101 | }); 102 | 103 | test(`should generate simple nested with required elements in lists`, function() { 104 | const { compileFromSource } = setup(starWarsSchema); 105 | const context = compileFromSource(` 106 | query StarshipCoords { 107 | starship { 108 | coordinates 109 | } 110 | } 111 | `); 112 | 113 | const source = generateSource(context); 114 | expect(source).toMatchSnapshot(); 115 | }); 116 | 117 | test(`should generate fragmented query operations`, function() { 118 | const { compileFromSource } = setup(starWarsSchema); 119 | const context = compileFromSource(` 120 | query HeroAndFriendsNames { 121 | hero { 122 | name 123 | ...heroFriends 124 | } 125 | } 126 | 127 | fragment heroFriends on Character { 128 | friends { 129 | name 130 | } 131 | } 132 | `); 133 | 134 | const source = generateSource(context); 135 | expect(source).toMatchSnapshot(); 136 | }); 137 | 138 | test(`should generate query operations with inline fragments`, function() { 139 | const { compileFromSource } = setup(starWarsSchema); 140 | const context = compileFromSource(` 141 | query HeroAndDetails { 142 | hero { 143 | name 144 | ...HeroDetails 145 | } 146 | } 147 | 148 | fragment HeroDetails on Character { 149 | ... on Droid { 150 | primaryFunction 151 | } 152 | ... on Human { 153 | height 154 | } 155 | } 156 | `); 157 | 158 | const source = generateSource(context); 159 | expect(source).toMatchSnapshot(); 160 | }); 161 | 162 | test(`should generate mutation operations with complex input types`, function() { 163 | const { compileFromSource } = setup(starWarsSchema); 164 | const context = compileFromSource(` 165 | mutation ReviewMovie($episode: Episode, $review: ReviewInput) { 166 | createReview(episode: $episode, review: $review) { 167 | stars 168 | commentary 169 | } 170 | } 171 | `); 172 | 173 | const source = generateSource(context); 174 | expect(source).toMatchSnapshot(); 175 | }); 176 | 177 | test(`should generate correct list with custom fragment`, function() { 178 | const { compileFromSource } = setup(starWarsSchema); 179 | const context = compileFromSource(` 180 | fragment Friend on Character { 181 | name 182 | } 183 | 184 | query HeroAndFriendsNames($episode: Episode) { 185 | hero(episode: $episode) { 186 | name 187 | friends { 188 | ...Friend 189 | } 190 | } 191 | } 192 | `); 193 | 194 | const source = generateSource(context); 195 | expect(source).toMatchSnapshot(); 196 | }); 197 | 198 | test('should handle single line comments', () => { 199 | const { compileFromSource } = setup(miscSchema); 200 | const context = compileFromSource(` 201 | query CustomScalar { 202 | commentTest { 203 | singleLine 204 | } 205 | } 206 | `); 207 | 208 | const source = generateSource(context); 209 | expect(source).toMatchSnapshot(); 210 | }); 211 | 212 | test('should handle multi-line comments', () => { 213 | const { compileFromSource } = setup(miscSchema); 214 | const context = compileFromSource(` 215 | query CustomScalar { 216 | commentTest { 217 | multiLine 218 | } 219 | } 220 | `); 221 | 222 | const source = generateSource(context); 223 | expect(source).toMatchSnapshot(); 224 | }); 225 | 226 | test('should handle comments in enums', () => { 227 | const { compileFromSource } = setup(miscSchema); 228 | const context = compileFromSource(` 229 | query CustomScalar { 230 | commentTest { 231 | enumCommentTest 232 | } 233 | } 234 | `); 235 | 236 | const source = generateSource(context); 237 | expect(source).toMatchSnapshot(); 238 | }); 239 | 240 | test('should handle interfaces at root', () => { 241 | const { compileFromSource } = setup(miscSchema); 242 | const context = compileFromSource(` 243 | query CustomScalar { 244 | interfaceTest { 245 | prop 246 | ... on ImplA { 247 | propA 248 | } 249 | ... on ImplB { 250 | propB 251 | } 252 | } 253 | } 254 | `); 255 | 256 | const source = generateSource(context); 257 | expect(source).toMatchSnapshot(); 258 | }); 259 | 260 | test('should handle unions at root', () => { 261 | const { compileFromSource } = setup(miscSchema); 262 | const context = compileFromSource(` 263 | query CustomScalar { 264 | unionTest { 265 | ... on PartialA { 266 | prop 267 | } 268 | ... on PartialB { 269 | prop 270 | } 271 | } 272 | } 273 | `); 274 | 275 | const source = generateSource(context); 276 | expect(source).toMatchSnapshot(); 277 | }); 278 | 279 | test('should have __typename value matching fragment type on generic type', () => { 280 | const { compileFromSource } = setup(starWarsSchema); 281 | const context = compileFromSource(` 282 | query HeroName { 283 | hero { 284 | ...HeroWithName 285 | } 286 | } 287 | 288 | fragment HeroWithName on Character { 289 | __typename 290 | name 291 | } 292 | `); 293 | 294 | const source = generateSource(context); 295 | expect(source).toMatchSnapshot(); 296 | }); 297 | 298 | test('should have __typename value matching fragment type on specific type', () => { 299 | const { compileFromSource } = setup(starWarsSchema); 300 | const context = compileFromSource(` 301 | query DroidName { 302 | droid { 303 | ...DroidWithName 304 | } 305 | } 306 | 307 | fragment DroidWithName on Droid { 308 | __typename 309 | name 310 | } 311 | `); 312 | 313 | const source = generateSource(context); 314 | expect(source).toMatchSnapshot(); 315 | }); 316 | }); 317 | }); 318 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | **Attention**: Please use the [AWS Amplify CLI](https://github.com/aws-amplify/amplify-cli) going forward for code generation with AWS AppSync projects. The new CLI has all codegen features of this project and more, including automatic graphql document generation and a GraphQL transformer. You can read more in the [tutorial for native app development](https://github.com/aws-amplify/amplify-cli/blob/master/native_guide.md). 6 | 7 | -- AWS team (October 1st, 2018) 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | # AWS Appsync GraphQL code generator 17 | 18 | [![GitHub license](https://img.shields.io/badge/license-MIT-lightgrey.svg?maxAge=2592000)](https://raw.githubusercontent.com/apollographql/apollo-ios/master/LICENSE) [![npm](https://img.shields.io/npm/v/aws-appsync-codegen.svg)](https://www.npmjs.com/package/apollo-codegen) [![Get on Slack](https://img.shields.io/badge/slack-join-orange.svg)](http://www.apollostack.com/#slack) 19 | 20 | This is a tool to generate API code or type annotations based on a GraphQL schema and query documents. This project is based upon [Apollo GraphQL code generator](https://github.com/apollographql/apollo-codegen). 21 | 22 | It currently generates Swift code, TypeScript annotations, Flow annotations, and Scala code. 23 | 24 | See [Apollo iOS](https://github.com/apollographql/apollo-ios) for details on the mapping from GraphQL results to Swift types, as well as runtime support for executing queries and mutations. 25 | 26 | ## Usage with AWS AppSync 27 | 28 | A complete tutorial can be found in the [AWS AppSync documentation](https://awslabs.github.io/aws-mobile-appsync-sdk-ios/) which is recommended for you to review first. 29 | 30 | ### Create GraphQL API 31 | 32 | If you have never created an AWS AppSync API before please use the [Quickstart Guide](https://docs.aws.amazon.com/appsync/latest/devguide/quickstart.html) and then walk through the [iOS client guide](https://docs.aws.amazon.com/appsync/latest/devguide/building-a-client-app-ios.html). 33 | 34 | ### Download introspection schema 35 | 36 | The code generaton process needs two things: 37 | 1. GraphQL introspection schema 38 | 1. GraphQL queries/mutations/subscriptions 39 | 40 | You can get the introspection schema in a `schema.json` file from the AWS AppSync console. You can find this in the console by clicking on your API name in the left-hand navigation, scrolling to the bottom, selecting **iOS**, clicking the **Export schema** dropdown, and selecting `schema.json`. 41 | 42 | ### Write GraphQL queries 43 | 44 | Now you can write GraphQL queries and the codegen process will convert these to native Swift types. If you are unfamiliar with writing a GraphQL query please [read through this guide](https://docs.aws.amazon.com/appsync/latest/devguide/quickstart-write-queries.html). Once you have your queries written, save them in a file called `queries.graphql`. For example you might have the following in your `queries.graphql` file: 45 | 46 | ```graphql 47 | query AllPosts { 48 | allPosts { 49 | id 50 | title 51 | author 52 | content 53 | url 54 | version 55 | } 56 | } 57 | ``` 58 | 59 | ### Generate Swift types 60 | 61 | Now that you have your introspection schema and your GraphQL query, install `aws-appsync-codegen` and run the tool against these two files like so: 62 | 63 | ```graphql 64 | npm install -g aws-appsync-codegen 65 | 66 | aws-appsync-codegen generate queries.graphql --schema schema.json --output API.swift 67 | ``` 68 | 69 | The output will be a Swift class called `API.swift` which you can include in your XCode project to perform a GraphQL query against AWS AppSync. 70 | 71 | ### Invoke GraphQL operation from Swift 72 | 73 | Now that you have completed the code generation, import the `API.swift` file into your XCode project. Then update your project's `Podfile` with a dependency of the AWS AppSync SDK: 74 | 75 | ``` 76 | target 'PostsApp' do 77 | use_frameworks! 78 | pod 'AWSAppSync' ~> '2.6.7' 79 | end 80 | ``` 81 | 82 | Next, in any code you wish to run the GraphQL query against AWS AppSync, import the SDK: 83 | 84 | ``` 85 | import AWSAppSync 86 | ``` 87 | 88 | Finally, run your query: 89 | 90 | ``` swift 91 | appSyncClient?.fetch(query: AllPostsQuery()) { (result, error) in 92 | if error != nil { 93 | print(error?.localizedDescription ?? "") 94 | return 95 | } 96 | self.postList = result?.data?.allPosts 97 | } 98 | ``` 99 | 100 | **Note:** The code generation process converted the GraphQL statement of `allPosts` in your `queries.graphql` file to `AllPostsQuery()` which allowed you to invoke this using `appSyncClient?.fetch()`. A similar process happens for mutations and subscriptions. 101 | 102 | ### Automate code generation 103 | 104 | The process defined above outlines the general flow, however you can [automate this in your XCode build process](https://docs.aws.amazon.com/appsync/latest/devguide/building-a-client-app-ios.html#building-a-client-app-integrating-into-the-build-process). 105 | 106 | ## General Usage 107 | 108 | If you want to experiment with the tool, you can install the `aws-appsync-codegen` command globally: 109 | 110 | ```sh 111 | npm install -g aws-appsync-codegen 112 | ``` 113 | 114 | ### `introspect-schema` 115 | 116 | The purpose of this command is to create a JSON introspection dump file for a given graphql schema. The input schema can be fetched from a remote graphql server or from a local file. The resulting JSON introspection dump file is needed as input to the [generate](#generate) command. 117 | 118 | To download a GraphQL schema by sending an introspection query to a server: 119 | 120 | ```sh 121 | aws-appsync-codegen introspect-schema http://localhost:8080/graphql --output schema.json 122 | ``` 123 | 124 | You can use the `header` option to add additional HTTP headers to the request. For example, to include an authentication token, use `--header "Authorization: Bearer "`. 125 | 126 | You can use the `insecure` option to ignore any SSL errors (for example if the server is running with self-signed certificate). 127 | 128 | **Note:** The command for downloading an introspection query was named `download-schema` but it was renamed to `introspect-schema` in order to have a single command for introspecting local or remote schemas. The old name `download-schema` is still available is an alias for backward compatibility. 129 | 130 | To generate a GraphQL schema introspection JSON from a local GraphQL schema: 131 | 132 | ```sh 133 | aws-appsync-codegen introspect-schema schema.graphql --output schema.json 134 | ``` 135 | 136 | ### `generate` 137 | 138 | The purpose of this command is to generate types for query and mutation operations made against the schema (it will not generate types for the schema itself). 139 | 140 | #### Swift 141 | 142 | This tool will generate Swift code by default from a set of query definitions in `.graphql` files: 143 | 144 | ```sh 145 | aws-appsync-codegen generate **/*.graphql --schema schema.json --output API.swift 146 | ``` 147 | 148 | The `--add-s3-wrapper` option can be specified to add in S3 wrapper code to the generated source. 149 | 150 | #### TypeScript, Flow, or Scala 151 | 152 | You can also generate type annotations for TypeScript, Flow, or Scala using the `--target` option: 153 | 154 | ```sh 155 | # TypeScript 156 | aws-appsync-codegen generate **/*.graphql --schema schema.json --target typescript --output operation-result-types.ts 157 | # Flow 158 | aws-appsync-codegen generate **/*.graphql --schema schema.json --target flow --output operation-result-types.flow.js 159 | # Scala 160 | aws-appsync-codegen generate **/*.graphql --schema schema.json --target scala --output operation-result-types.scala 161 | ``` 162 | 163 | #### `gql` template support 164 | 165 | If the source file for generation is a javascript or typescript file, the codegen will try to extrapolate the queries inside the [gql tag](https://github.com/apollographql/graphql-tag) templates. 166 | 167 | The tag name is configurable using the CLI `--tag-name` option. 168 | 169 | #### [.graphqlconfig](https://github.com/graphcool/graphql-config) support 170 | 171 | Instead of using the `--schema` option to point out you GraphQL schema, you can specify it in a `.graphqlconfig` file. 172 | 173 | In case you specify multiple schemas in your `.graphqlconfig` file, choose which one to pick by using the `--project-name` option. 174 | 175 | ## Typescript and Flow 176 | 177 | When using `aws-appsync-codegen` with Typescript or Flow, make sure to add the `__typename` introspection field to every selection set within your graphql operations. 178 | 179 | If you're using a client like `apollo-client` that does this automatically for your GraphQL operations, pass in the `--addTypename` option to `aws-appsync-codegen` to make sure the generated Typescript and Flow types have the `__typename` field as well. This is required to ensure proper type generation support for `GraphQLUnionType` and `GraphQLInterfaceType` fields. 180 | 181 | ### Why is the __typename field required? 182 | 183 | Using the type information from the GraphQL schema, we can infer the possible types for fields. However, in the case of a `GraphQLUnionType` or `GraphQLInterfaceType`, there are multiple types that are possible for that field. This is best modeled using a disjoint union with the `__typename` 184 | as the discriminant. 185 | 186 | For example, given a schema: 187 | ```graphql 188 | ... 189 | 190 | interface Character { 191 | name: String! 192 | } 193 | 194 | type Human implements Character { 195 | homePlanet: String 196 | } 197 | 198 | type Droid implements Character { 199 | primaryFunction: String 200 | } 201 | 202 | ... 203 | ``` 204 | 205 | Whenever a field of type `Character` is encountered, it could be either a Human or Droid. Human and Droid objects 206 | will have a different set of fields. Within your application code, when interacting with a `Character` you'll want to make sure to handle both of these cases. 207 | 208 | Given this query: 209 | 210 | ```graphql 211 | query Characters { 212 | characters(episode: NEW_HOPE) { 213 | name 214 | 215 | ... on Human { 216 | homePlanet 217 | } 218 | 219 | ... on Droid { 220 | primaryFunction 221 | } 222 | } 223 | } 224 | ``` 225 | 226 | Apollo Codegen will generate a union type for Character. 227 | 228 | ```javascript 229 | export type CharactersQuery = { 230 | characters: Array<{ 231 | __typename: 'Human', 232 | name: string, 233 | homePlanet: ?string 234 | } | { 235 | __typename: 'Droid', 236 | name: string, 237 | primaryFunction: ?string 238 | }> 239 | } 240 | ``` 241 | 242 | This type can then be used as follows to ensure that all possible types are handled: 243 | 244 | ```javascript 245 | function CharacterFigures({ characters }: CharactersQuery) { 246 | return characters.map(character => { 247 | switch(character.__typename) { 248 | case "Human": 249 | return 250 | case "Droid": 251 | return 252 | } 253 | }); 254 | } 255 | ``` 256 | 257 | ## Contributing 258 | 259 | Running tests locally: 260 | 261 | ``` 262 | npm install 263 | npm test 264 | ``` 265 | -------------------------------------------------------------------------------- /test/flow/codeGeneration.js: -------------------------------------------------------------------------------- 1 | import { 2 | parse, 3 | isType, 4 | GraphQLID, 5 | GraphQLString, 6 | GraphQLInt, 7 | GraphQLList, 8 | GraphQLNonNull 9 | } from 'graphql'; 10 | 11 | import { 12 | generateSource 13 | } from '../../src/flow/codeGeneration'; 14 | 15 | import { loadSchema } from '../../src/loading'; 16 | const starWarsSchema = loadSchema(require.resolve('../fixtures/starwars/schema.json')); 17 | const miscSchema = loadSchema(require.resolve('../fixtures/misc/schema.json')); 18 | 19 | import CodeGenerator from '../../src/utilities/CodeGenerator'; 20 | 21 | import { compileToLegacyIR } from '../../src/compiler/legacyIR'; 22 | 23 | function setup(schema) { 24 | const context = { 25 | schema: schema, 26 | operations: {}, 27 | fragments: {}, 28 | typesUsed: {} 29 | } 30 | 31 | const generator = new CodeGenerator(context); 32 | 33 | const compileFromSource = (source) => { 34 | const document = parse(source); 35 | const context = compileToLegacyIR(schema, document, { mergeInFieldsFromFragmentSpreads: true, addTypename: true } ); 36 | generator.context = context; 37 | return context; 38 | }; 39 | 40 | const addFragment = (fragment) => { 41 | generator.context.fragments[fragment.fragmentName] = fragment; 42 | }; 43 | 44 | return { generator, compileFromSource, addFragment }; 45 | } 46 | 47 | describe('Flow code generation', function() { 48 | describe('#generateSource()', function() { 49 | test(`should generate simple query operations`, function() { 50 | const { compileFromSource } = setup(starWarsSchema); 51 | const context = compileFromSource(` 52 | query HeroName { 53 | hero { 54 | name 55 | } 56 | } 57 | `); 58 | 59 | const source = generateSource(context); 60 | expect(source).toMatchSnapshot(); 61 | }); 62 | 63 | test(`should generate simple query operations including input variables`, function() { 64 | const { compileFromSource } = setup(starWarsSchema); 65 | const context = compileFromSource(` 66 | query HeroName($episode: Episode) { 67 | hero(episode: $episode) { 68 | name 69 | } 70 | } 71 | `); 72 | 73 | const source = generateSource(context); 74 | expect(source).toMatchSnapshot(); 75 | }); 76 | 77 | test(`should generate simple nested query operations including input variables`, function() { 78 | const { compileFromSource } = setup(starWarsSchema); 79 | const context = compileFromSource(` 80 | query HeroAndFriendsNames($episode: Episode) { 81 | hero(episode: $episode) { 82 | name 83 | friends { 84 | name 85 | } 86 | } 87 | } 88 | `); 89 | 90 | const source = generateSource(context); 91 | expect(source).toMatchSnapshot(); 92 | }); 93 | 94 | test(`should generate array query operations`, function() { 95 | const { compileFromSource } = setup(starWarsSchema); 96 | const context = compileFromSource(` 97 | query ReviewsStars { 98 | reviews { 99 | stars 100 | } 101 | } 102 | `); 103 | 104 | const source = generateSource(context); 105 | expect(source).toMatchSnapshot(); 106 | }); 107 | 108 | test(`should generate simple nested with required elements in lists`, function() { 109 | const { compileFromSource } = setup(starWarsSchema); 110 | const context = compileFromSource(` 111 | query StarshipCoords { 112 | starship { 113 | coordinates 114 | } 115 | } 116 | `); 117 | 118 | const source = generateSource(context); 119 | expect(source).toMatchSnapshot(); 120 | }); 121 | 122 | test(`should generate fragmented query operations`, function() { 123 | const { compileFromSource } = setup(starWarsSchema); 124 | const context = compileFromSource(` 125 | query HeroAndFriendsNames { 126 | hero { 127 | name 128 | ...heroFriends 129 | } 130 | } 131 | 132 | fragment heroFriends on Character { 133 | friends { 134 | name 135 | } 136 | } 137 | `); 138 | 139 | const source = generateSource(context); 140 | expect(source).toMatchSnapshot(); 141 | }); 142 | 143 | test(`should generate query operations with inline fragments`, function() { 144 | const { compileFromSource } = setup(starWarsSchema); 145 | const context = compileFromSource(` 146 | query HeroAndDetails { 147 | hero { 148 | name 149 | ...HeroDetails 150 | } 151 | } 152 | 153 | fragment HeroDetails on Character { 154 | ... on Droid { 155 | primaryFunction 156 | } 157 | ... on Human { 158 | height 159 | } 160 | } 161 | `); 162 | 163 | const source = generateSource(context); 164 | expect(source).toMatchSnapshot(); 165 | }); 166 | 167 | test(`should generate mutation operations with complex input types`, function() { 168 | const { compileFromSource } = setup(starWarsSchema); 169 | const context = compileFromSource(` 170 | mutation ReviewMovie($episode: Episode, $review: ReviewInput) { 171 | createReview(episode: $episode, review: $review) { 172 | stars 173 | commentary 174 | } 175 | } 176 | `); 177 | 178 | const source = generateSource(context); 179 | expect(source).toMatchSnapshot(); 180 | }); 181 | 182 | test(`should generate correct typedefs with a single custom fragment`, function() { 183 | const { compileFromSource } = setup(starWarsSchema); 184 | const context = compileFromSource(` 185 | fragment Friend on Character { 186 | name 187 | } 188 | 189 | query HeroAndFriendsNames($episode: Episode) { 190 | hero(episode: $episode) { 191 | name 192 | friends { 193 | ...Friend 194 | } 195 | } 196 | } 197 | `); 198 | 199 | const source = generateSource(context); 200 | expect(source).toMatchSnapshot(); 201 | }); 202 | 203 | test(`should generate correct typedefs with a multiple custom fragments`, function() { 204 | const { compileFromSource } = setup(starWarsSchema); 205 | const context = compileFromSource(` 206 | fragment Friend on Character { 207 | name 208 | } 209 | 210 | fragment Person on Character { 211 | name 212 | } 213 | 214 | query HeroAndFriendsNames($episode: Episode) { 215 | hero(episode: $episode) { 216 | name 217 | friends { 218 | ...Friend 219 | ...Person 220 | } 221 | } 222 | } 223 | `); 224 | 225 | const source = generateSource(context); 226 | expect(source).toMatchSnapshot(); 227 | }); 228 | 229 | test(`should annotate custom scalars as string`, function() { 230 | const { compileFromSource } = setup(miscSchema); 231 | const context = compileFromSource(` 232 | query CustomScalar { 233 | misc { 234 | date 235 | } 236 | } 237 | `); 238 | 239 | const source = generateSource(context); 240 | expect(source).toMatchSnapshot(); 241 | }); 242 | 243 | test('should handle single line comments', () => { 244 | const { compileFromSource } = setup(miscSchema); 245 | const context = compileFromSource(` 246 | query CustomScalar { 247 | commentTest { 248 | singleLine 249 | } 250 | } 251 | `); 252 | 253 | const source = generateSource(context); 254 | expect(source).toMatchSnapshot(); 255 | }); 256 | 257 | test('should handle multi-line comments', () => { 258 | const { compileFromSource } = setup(miscSchema); 259 | const context = compileFromSource(` 260 | query CustomScalar { 261 | commentTest { 262 | multiLine 263 | } 264 | } 265 | `); 266 | 267 | const source = generateSource(context); 268 | expect(source).toMatchSnapshot(); 269 | }); 270 | 271 | test('should handle comments in enums', () => { 272 | const { compileFromSource } = setup(miscSchema); 273 | const context = compileFromSource(` 274 | query CustomScalar { 275 | commentTest { 276 | enumCommentTest 277 | } 278 | } 279 | `); 280 | 281 | const source = generateSource(context); 282 | expect(source).toMatchSnapshot(); 283 | }); 284 | 285 | test('should handle interfaces at root', () => { 286 | const { compileFromSource } = setup(miscSchema); 287 | const context = compileFromSource(` 288 | query CustomScalar { 289 | interfaceTest { 290 | prop 291 | ... on ImplA { 292 | propA 293 | } 294 | ... on ImplB { 295 | propB 296 | } 297 | } 298 | } 299 | `); 300 | 301 | const source = generateSource(context); 302 | expect(source).toMatchSnapshot(); 303 | }); 304 | 305 | test('should handle unions at root', () => { 306 | const { compileFromSource } = setup(miscSchema); 307 | const context = compileFromSource(` 308 | query CustomScalar { 309 | unionTest { 310 | ... on PartialA { 311 | prop 312 | } 313 | ... on PartialB { 314 | prop 315 | } 316 | } 317 | } 318 | `); 319 | 320 | const source = generateSource(context); 321 | expect(source).toMatchSnapshot(); 322 | }); 323 | 324 | test('should handle scalars at root', () => { 325 | const { compileFromSource } = setup(miscSchema); 326 | const context = compileFromSource(` 327 | query RootScalar { 328 | scalarTest 329 | } 330 | `); 331 | 332 | const source = generateSource(context); 333 | expect(source).toMatchSnapshot(); 334 | }); 335 | 336 | test('should have __typename value matching fragment type on generic type', () => { 337 | const { compileFromSource } = setup(starWarsSchema); 338 | const context = compileFromSource(` 339 | query HeroName { 340 | hero { 341 | ...HeroWithName 342 | } 343 | } 344 | 345 | fragment HeroWithName on Character { 346 | __typename 347 | name 348 | } 349 | `); 350 | 351 | const source = generateSource(context); 352 | expect(source).toMatchSnapshot(); 353 | }); 354 | 355 | test('should have __typename value matching fragment type on specific type', () => { 356 | const { compileFromSource } = setup(starWarsSchema); 357 | const context = compileFromSource(` 358 | query DroidName { 359 | droid { 360 | ...DroidWithName 361 | } 362 | } 363 | 364 | fragment DroidWithName on Droid { 365 | __typename 366 | name 367 | } 368 | `); 369 | 370 | const source = generateSource(context); 371 | expect(source).toMatchSnapshot(); 372 | }); 373 | }); 374 | }); 375 | -------------------------------------------------------------------------------- /src/flow-modern/codeGeneration.ts: -------------------------------------------------------------------------------- 1 | import * as t from '@babel/types'; 2 | import { stripIndent } from 'common-tags'; 3 | import { 4 | GraphQLEnumType, 5 | GraphQLInputObjectType, 6 | GraphQLNonNull, 7 | } from 'graphql'; 8 | import * as path from 'path'; 9 | 10 | import { 11 | CompilerContext, 12 | Operation, 13 | Fragment, 14 | SelectionSet, 15 | Field, 16 | } from '../compiler'; 17 | 18 | import { 19 | typeCaseForSelectionSet, 20 | Variant 21 | } from '../compiler/visitors/typeCase'; 22 | 23 | import { 24 | collectAndMergeFields 25 | } from '../compiler/visitors/collectAndMergeFields'; 26 | 27 | import { BasicGeneratedFile } from '../utilities/CodeGenerator'; 28 | import FlowGenerator, { ObjectProperty, FlowCompilerOptions, } from './language'; 29 | import Printer from './printer'; 30 | 31 | class FlowGeneratedFile implements BasicGeneratedFile { 32 | fileContents: string; 33 | 34 | constructor(fileContents: string) { 35 | this.fileContents = fileContents; 36 | } 37 | get output() { 38 | return this.fileContents 39 | } 40 | } 41 | 42 | function printEnumsAndInputObjects(generator: FlowAPIGenerator, context: CompilerContext) { 43 | generator.printer.enqueue(stripIndent` 44 | //============================================================== 45 | // START Enums and Input Objects 46 | // All enums and input objects are included in every output file 47 | // for now, but this will be changed soon. 48 | // TODO: Link to issue to fix this. 49 | //============================================================== 50 | `); 51 | 52 | context.typesUsed 53 | .filter(type => (type instanceof GraphQLEnumType)) 54 | .forEach((enumType) => { 55 | generator.typeAliasForEnumType(enumType as GraphQLEnumType); 56 | }); 57 | 58 | context.typesUsed 59 | .filter(type => type instanceof GraphQLInputObjectType) 60 | .forEach((inputObjectType) => { 61 | generator.typeAliasForInputObjectType(inputObjectType as GraphQLInputObjectType); 62 | }); 63 | 64 | generator.printer.enqueue(stripIndent` 65 | //============================================================== 66 | // END Enums and Input Objects 67 | //============================================================== 68 | `) 69 | } 70 | 71 | export function generateSource( 72 | context: CompilerContext, 73 | ) { 74 | const generator = new FlowAPIGenerator(context); 75 | const generatedFiles: { [filePath: string]: FlowGeneratedFile } = {}; 76 | 77 | Object.values(context.operations) 78 | .forEach((operation) => { 79 | generator.fileHeader(); 80 | generator.typeAliasesForOperation(operation); 81 | printEnumsAndInputObjects(generator, context); 82 | 83 | const output = generator.printer.printAndClear(); 84 | 85 | const outputFilePath = path.join( 86 | path.dirname(operation.filePath), 87 | '__generated__', 88 | `${operation.operationName}.js` 89 | ); 90 | 91 | generatedFiles[outputFilePath] = new FlowGeneratedFile(output); 92 | }); 93 | 94 | Object.values(context.fragments) 95 | .forEach((fragment) => { 96 | generator.fileHeader(); 97 | generator.typeAliasesForFragment(fragment); 98 | printEnumsAndInputObjects(generator, context); 99 | 100 | const output = generator.printer.printAndClear(); 101 | 102 | const outputFilePath = path.join( 103 | path.dirname(fragment.filePath), 104 | '__generated__', 105 | `${fragment.fragmentName}.js` 106 | ); 107 | 108 | generatedFiles[outputFilePath] = new FlowGeneratedFile(output); 109 | }); 110 | 111 | return generatedFiles; 112 | } 113 | 114 | export class FlowAPIGenerator extends FlowGenerator { 115 | context: CompilerContext 116 | printer: Printer 117 | scopeStack: string[] 118 | 119 | constructor(context: CompilerContext) { 120 | super(context.options as FlowCompilerOptions); 121 | 122 | this.context = context; 123 | this.printer = new Printer(); 124 | this.scopeStack = []; 125 | } 126 | 127 | fileHeader() { 128 | this.printer.enqueue( 129 | stripIndent` 130 | /* @flow */ 131 | // This file was automatically generated and should not be edited. 132 | ` 133 | ); 134 | } 135 | 136 | public typeAliasForEnumType(enumType: GraphQLEnumType) { 137 | this.printer.enqueue(this.enumerationDeclaration(enumType)); 138 | } 139 | 140 | public typeAliasForInputObjectType(inputObjectType: GraphQLInputObjectType) { 141 | this.printer.enqueue(this.inputObjectDeclaration(inputObjectType)); 142 | } 143 | 144 | public typeAliasesForOperation(operation: Operation) { 145 | const { 146 | operationType, 147 | operationName, 148 | selectionSet 149 | } = operation; 150 | 151 | this.scopeStackPush(operationName); 152 | 153 | this.printer.enqueue(stripIndent` 154 | // ==================================================== 155 | // GraphQL ${operationType} operation: ${operationName} 156 | // ==================================================== 157 | `) 158 | 159 | // The root operation only has one variant 160 | // Do we need to get exhaustive variants anyway? 161 | const variants = this.getVariantsForSelectionSet(selectionSet); 162 | 163 | const variant = variants[0]; 164 | const properties = this.getPropertiesForVariant(variant); 165 | 166 | const exportedTypeAlias = this.exportDeclaration( 167 | this.typeAliasObject(operationName, properties) 168 | ); 169 | 170 | this.printer.enqueue(exportedTypeAlias); 171 | this.scopeStackPop(); 172 | } 173 | 174 | public typeAliasesForFragment(fragment: Fragment) { 175 | const { 176 | fragmentName, 177 | selectionSet 178 | } = fragment; 179 | 180 | this.scopeStackPush(fragmentName); 181 | 182 | this.printer.enqueue(stripIndent` 183 | // ==================================================== 184 | // GraphQL fragment: ${fragmentName} 185 | // ==================================================== 186 | `); 187 | 188 | const variants = this.getVariantsForSelectionSet(selectionSet); 189 | 190 | if (variants.length === 1) { 191 | const properties = this.getPropertiesForVariant(variants[0]); 192 | 193 | const name = this.annotationFromScopeStack(this.scopeStack).id.name; 194 | const exportedTypeAlias = this.exportDeclaration( 195 | this.typeAliasObject( 196 | name, 197 | properties 198 | ) 199 | ); 200 | 201 | this.printer.enqueue(exportedTypeAlias); 202 | } else { 203 | const unionMembers: t.FlowTypeAnnotation[] = []; 204 | variants.forEach(variant => { 205 | this.scopeStackPush(variant.possibleTypes[0].toString()); 206 | const properties = this.getPropertiesForVariant(variant); 207 | 208 | const name = this.annotationFromScopeStack(this.scopeStack).id.name; 209 | const exportedTypeAlias = this.exportDeclaration( 210 | this.typeAliasObject( 211 | name, 212 | properties 213 | ) 214 | ); 215 | 216 | this.printer.enqueue(exportedTypeAlias); 217 | 218 | unionMembers.push(this.annotationFromScopeStack(this.scopeStack)); 219 | 220 | this.scopeStackPop(); 221 | }); 222 | 223 | this.printer.enqueue( 224 | this.exportDeclaration( 225 | this.typeAliasGenericUnion( 226 | this.annotationFromScopeStack(this.scopeStack).id.name, 227 | unionMembers 228 | ) 229 | ) 230 | ); 231 | } 232 | 233 | this.scopeStackPop(); 234 | } 235 | 236 | private getVariantsForSelectionSet(selectionSet: SelectionSet) { 237 | return this.getTypeCasesForSelectionSet(selectionSet).exhaustiveVariants; 238 | } 239 | 240 | private getTypeCasesForSelectionSet(selectionSet: SelectionSet) { 241 | return typeCaseForSelectionSet( 242 | selectionSet, 243 | this.context.options.mergeInFieldsFromFragmentSpreads 244 | ); 245 | } 246 | 247 | private getPropertiesForVariant(variant: Variant): ObjectProperty[] { 248 | const fields = collectAndMergeFields( 249 | variant, 250 | this.context.options.mergeInFieldsFromFragmentSpreads 251 | ); 252 | 253 | return fields.map(field => { 254 | const fieldName = field.alias !== undefined ? field.alias : field.name; 255 | this.scopeStackPush(fieldName); 256 | 257 | let res; 258 | if (field.selectionSet) { 259 | const genericAnnotation = this.annotationFromScopeStack(this.scopeStack); 260 | if (field.type instanceof GraphQLNonNull) { 261 | genericAnnotation.id.name = genericAnnotation.id.name; 262 | } else { 263 | genericAnnotation.id.name = '?' + genericAnnotation.id.name; 264 | } 265 | 266 | res = this.handleFieldSelectionSetValue( 267 | genericAnnotation, 268 | field 269 | ); 270 | } else { 271 | res = this.handleFieldValue( 272 | field, 273 | variant 274 | ); 275 | } 276 | 277 | this.scopeStackPop(); 278 | return res; 279 | }); 280 | } 281 | 282 | private handleFieldSelectionSetValue(genericAnnotation: t.GenericTypeAnnotation, field: Field) { 283 | const { selectionSet } = field; 284 | 285 | const typeCase = this.getTypeCasesForSelectionSet(selectionSet as SelectionSet); 286 | const variants = typeCase.exhaustiveVariants; 287 | 288 | let exportedTypeAlias; 289 | if (variants.length === 1) { 290 | const variant = variants[0]; 291 | const properties = this.getPropertiesForVariant(variant); 292 | exportedTypeAlias = this.exportDeclaration( 293 | this.typeAliasObject( 294 | this.annotationFromScopeStack(this.scopeStack).id.name, 295 | properties 296 | ) 297 | ); 298 | } else { 299 | const propertySets = variants.map(variant => { 300 | this.scopeStackPush(variant.possibleTypes[0].toString()) 301 | const properties = this.getPropertiesForVariant(variant); 302 | this.scopeStackPop(); 303 | return properties; 304 | }) 305 | 306 | exportedTypeAlias = this.exportDeclaration( 307 | this.typeAliasObjectUnion( 308 | genericAnnotation.id.name, 309 | propertySets 310 | ) 311 | ); 312 | } 313 | 314 | this.printer.enqueue(exportedTypeAlias); 315 | 316 | return { 317 | name: field.alias ? field.alias : field.name, 318 | description: field.description, 319 | annotation: genericAnnotation 320 | }; 321 | } 322 | 323 | private handleFieldValue(field: Field, variant: Variant) { 324 | let res; 325 | if (field.name === '__typename') { 326 | const annotations = variant.possibleTypes 327 | .map(type => { 328 | const annotation = t.stringLiteralTypeAnnotation(); 329 | annotation.value = type.toString(); 330 | return annotation; 331 | }); 332 | 333 | res = { 334 | name: field.alias ? field.alias : field.name, 335 | description: field.description, 336 | annotation: t.unionTypeAnnotation(annotations) 337 | }; 338 | } else { 339 | // TODO: Double check that this works 340 | res = { 341 | name: field.alias ? field.alias : field.name, 342 | description: field.description, 343 | annotation: this.typeAnnotationFromGraphQLType(field.type) 344 | }; 345 | } 346 | 347 | return res; 348 | } 349 | 350 | public get output(): string { 351 | return this.printer.print(); 352 | } 353 | 354 | scopeStackPush(name: string) { 355 | this.scopeStack.push(name); 356 | } 357 | 358 | scopeStackPop() { 359 | const popped = this.scopeStack.pop() 360 | return popped; 361 | } 362 | 363 | } 364 | --------------------------------------------------------------------------------