├── .gitignore ├── README.md ├── example ├── index.ts ├── test.ts └── tsconfig.json ├── package-lock.json ├── package.json ├── src ├── createSchema.ts ├── date.ts ├── index.ts ├── tsconfig.json └── types.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | dist 4 | .idea 5 | ts2graphql.code-workspace 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ts2graphql 2 | 3 | This tool converts your typescript interfaces to graphql schema. 4 | 5 | // schema.ts 6 | 7 | ```ts 8 | import { ID, Int, Float } from 'ts2graphql'; 9 | interface Query { 10 | getByAuthor?(args: { id: ID }): Author; 11 | } 12 | 13 | interface Author { 14 | id: ID; 15 | name: string; 16 | books(filter: { max?: Int }): Book[]; 17 | } 18 | 19 | type Book = PDFBook | AudioBook; 20 | 21 | interface BookBase { 22 | /** The authors of this content */ 23 | authors: Author[]; 24 | name: string; 25 | publishedAt: Date; 26 | illustrator?: { 27 | __typename: 'Illustrator'; 28 | name: string; 29 | email: string; 30 | }; 31 | } 32 | 33 | interface PDFBook extends BookBase { 34 | file: string; 35 | } 36 | 37 | interface AudioBook extends BookBase { 38 | audioFile: string; 39 | } 40 | ``` 41 | 42 | // index.ts 43 | ```ts 44 | import { printSchema } from 'graphql'; 45 | import { createSchema } from 'ts2graphql'; 46 | 47 | const schema = createSchema(__dirname + '/schema.ts'); 48 | const rootValue = {}; // you should implement resolve methods 49 | express.use( 50 | '/api/graphql', 51 | graphqlHTTP({ 52 | schema: schema, 53 | rootValue: rootValue, 54 | }) 55 | ); 56 | 57 | console.log(printSchema(schema)); 58 | ``` 59 | will generate schema 60 | ```graphql 61 | type AudioBook { 62 | audioFile: String! 63 | 64 | """The authors of this content""" 65 | authors: [Author!]! 66 | name: String! 67 | publishedAt: Date! 68 | illustrator: Illustrator 69 | } 70 | 71 | type Author { 72 | id: ID! 73 | name: String! 74 | books(max: Int): [Book!]! 75 | } 76 | 77 | union Book = PDFBook | AudioBook 78 | 79 | scalar Date 80 | 81 | type Illustrator { 82 | __typename: String! 83 | name: String! 84 | email: String! 85 | } 86 | 87 | type PDFBook { 88 | file: String! 89 | 90 | """The authors of this content""" 91 | authors: [Author!]! 92 | name: String! 93 | publishedAt: Date! 94 | illustrator: Illustrator 95 | } 96 | 97 | type Query { 98 | getByAuthor(id: ID!): Author 99 | } 100 | 101 | ``` 102 | -------------------------------------------------------------------------------- /example/index.ts: -------------------------------------------------------------------------------- 1 | import { createSchema } from '../src/createSchema'; 2 | import { printSchema } from 'graphql'; 3 | 4 | const schema = createSchema(__dirname + '/test.ts'); 5 | console.log(printSchema(schema)); 6 | -------------------------------------------------------------------------------- /example/test.ts: -------------------------------------------------------------------------------- 1 | type ID = number; 2 | type Float = number; 3 | type Int = number; 4 | interface MainQuery { 5 | foo: Foo; 6 | } 7 | interface Foo { 8 | __typename: 'Foo'; 9 | id: ID; 10 | name: string; 11 | value?: number; 12 | size: Int; 13 | br: Bar; 14 | baz: Baz; 15 | coord: { 16 | __typename: 'Coord'; 17 | /** Hey */ 18 | x: Float; 19 | y: Float; 20 | }; 21 | } 22 | 23 | type Union = Foo | Bar; 24 | 25 | /** 26 | * Bar doc 27 | */ 28 | interface Bar extends Foo { 29 | /** Doc for bar */ 30 | bar?: string; 31 | items: Foo[]; 32 | items2?: Foo[][]; 33 | /** 34 | * Long doc for hi 35 | */ 36 | hi?: Union; 37 | } 38 | 39 | interface Baz { 40 | retInt(args: { a?: Int; b?: string; c?: boolean; d: boolean }): Int; 41 | foo(args: { 42 | /** some doc */ 43 | foo?: number; 44 | }): Bar; 45 | } 46 | 47 | interface Query { 48 | mainQuery: MainQuery; 49 | } -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": {} 4 | } 5 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts2graphql", 3 | "version": "1.0.10", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/graphql": { 8 | "version": "14.0.5", 9 | "resolved": "https://registry.npmjs.org/@types/graphql/-/graphql-14.0.5.tgz", 10 | "integrity": "sha512-bwGYLE0SRy5ZraC91dqI2bxbspfm10kyJ2Yjuvk4OjdGznh7fkoWW+xXZHfFydJaqu9syZi099cpiZw3GlPDiA==" 11 | }, 12 | "@types/node": { 13 | "version": "10.12.18", 14 | "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.18.tgz", 15 | "integrity": "sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==" 16 | }, 17 | "arg": { 18 | "version": "4.1.0", 19 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.0.tgz", 20 | "integrity": "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg==", 21 | "dev": true 22 | }, 23 | "buffer-from": { 24 | "version": "1.1.1", 25 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 26 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", 27 | "dev": true 28 | }, 29 | "diff": { 30 | "version": "3.5.0", 31 | "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", 32 | "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", 33 | "dev": true 34 | }, 35 | "make-error": { 36 | "version": "1.3.5", 37 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", 38 | "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", 39 | "dev": true 40 | }, 41 | "source-map": { 42 | "version": "0.6.1", 43 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 44 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 45 | "dev": true 46 | }, 47 | "source-map-support": { 48 | "version": "0.5.10", 49 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.10.tgz", 50 | "integrity": "sha512-YfQ3tQFTK/yzlGJuX8pTwa4tifQj4QS2Mj7UegOu8jAz59MqIiMGPXxQhVQiIMNzayuUSF/jEuVnfFF5JqybmQ==", 51 | "dev": true, 52 | "requires": { 53 | "buffer-from": "^1.0.0", 54 | "source-map": "^0.6.0" 55 | } 56 | }, 57 | "ts-node": { 58 | "version": "8.0.2", 59 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.0.2.tgz", 60 | "integrity": "sha512-MosTrinKmaAcWgO8tqMjMJB22h+sp3Rd1i4fdoWY4mhBDekOwIAKI/bzmRi7IcbCmjquccYg2gcF6NBkLgr0Tw==", 61 | "dev": true, 62 | "requires": { 63 | "arg": "^4.1.0", 64 | "diff": "^3.1.0", 65 | "make-error": "^1.1.1", 66 | "source-map-support": "^0.5.6", 67 | "yn": "^3.0.0" 68 | } 69 | }, 70 | "ts-type-ast": { 71 | "version": "1.0.7", 72 | "resolved": "https://registry.npmjs.org/ts-type-ast/-/ts-type-ast-1.0.7.tgz", 73 | "integrity": "sha512-HQZmbrZCg9kV/uFJcSFMOCj0Rm/qrWmH7iwinaKsYqxbELswuYjhPtrJcGzX2CKFHd5oe3n1jmwfq5sWyEhFMA==", 74 | "requires": { 75 | "@types/node": ">8.0.0", 76 | "typescript": ">3.0.0" 77 | } 78 | }, 79 | "typescript": { 80 | "version": "3.4.5", 81 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.5.tgz", 82 | "integrity": "sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw==" 83 | }, 84 | "yn": { 85 | "version": "3.0.0", 86 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.0.0.tgz", 87 | "integrity": "sha512-+Wo/p5VRfxUgBUGy2j/6KX2mj9AYJWOHuhMjMcbBFc3y54o9/4buK1ksBvuiK01C3kby8DH9lSmJdSxw+4G/2Q==", 88 | "dev": true 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts2graphql", 3 | "version": "1.0.11", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "types": "dist/index", 7 | "repository": { 8 | "type": "github", 9 | "url": "https://github.com/cevek/ts2graphql" 10 | }, 11 | "scripts": { 12 | "build": "tsc -p src", 13 | "prepublishOnly": "rm -rf dist && npm run build", 14 | "example": "npx ts-node example/index.ts" 15 | }, 16 | "keywords": [ 17 | "typescript", 18 | "graphql", 19 | "interface" 20 | ], 21 | "author": "cevek", 22 | "license": "ISC", 23 | "files": [ 24 | "dist" 25 | ], 26 | "dependencies": { 27 | "@types/graphql": ">=14.0.5", 28 | "@types/node": ">=10.0.0", 29 | "ts-type-ast": ">=1.0.7" 30 | }, 31 | "peerDependencies": { 32 | "typescript": ">3.3.0", 33 | "graphql": ">14.0.0" 34 | }, 35 | "devDependencies": { 36 | "ts-node": ">=8.0.2", 37 | "typescript": ">=3.4.5" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/createSchema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLBoolean, 3 | GraphQLFieldConfigArgumentMap, 4 | GraphQLFieldConfigMap, 5 | GraphQLFloat, 6 | GraphQLID, 7 | GraphQLInputObjectType, 8 | GraphQLInputType, 9 | GraphQLInt, 10 | GraphQLList, 11 | GraphQLNonNull, 12 | GraphQLObjectType, 13 | GraphQLOutputType, 14 | GraphQLScalarType, 15 | GraphQLSchema, 16 | GraphQLString, 17 | GraphQLType, 18 | GraphQLUnionType, 19 | GraphQLError, 20 | Kind, 21 | } from 'graphql'; 22 | import * as ts from 'typescript'; 23 | import {DateType} from './date'; 24 | import {typeAST, AllTypes, Interface, Primitive, Union, InterfaceLiteral, UnionLiteral} from 'ts-type-ast'; 25 | 26 | type CustomScalarFactory = (type: Primitive) => GraphQLScalarType | undefined; 27 | export function createSchema( 28 | fileName: string, 29 | options: {customScalars?: GraphQLScalarType[]; customScalarFactory?: CustomScalarFactory} = {}, 30 | ) { 31 | const customScalarsMap = new Map(); 32 | (options.customScalars || []).forEach(value => customScalarsMap.set(value.name, value)); 33 | const customScalar = options.customScalarFactory; 34 | 35 | const program = ts.createProgram({options: {strict: true}, rootNames: [fileName]}); 36 | const checker = program.getTypeChecker(); 37 | const sourceFile = program.getSourceFile(fileName)!; 38 | //@ts-ignore 39 | const types = typeAST(checker, sourceFile); 40 | const map = new Map(); 41 | let anonTypeIdx = 0; 42 | 43 | const schema = createSchemaFromTypes(); 44 | return schema; 45 | 46 | function createSchemaFromTypes() { 47 | let query!: GraphQLObjectType; 48 | let mutation: GraphQLObjectType | undefined; 49 | for (let i = 0; i < types.length; i++) { 50 | const type = types[i]; 51 | if (type.kind === 'interface' && (type.name === 'Query' || type.name === 'Mutation')) { 52 | const gqlType = createGQL(types[i], false); 53 | if (gqlType instanceof GraphQLObjectType) { 54 | if (type.name === 'Query') { 55 | query = gqlType; 56 | } 57 | if (type.name === 'Mutation') { 58 | mutation = gqlType; 59 | } 60 | } 61 | } 62 | } 63 | if (!(query||mutation)) throw new Error("No 'Query' or 'Mutation' type found"); 64 | return new GraphQLSchema({ 65 | query: query, 66 | mutation: mutation, 67 | }); 68 | } 69 | 70 | function add(type: AllTypes, gqltype: GraphQLType) { 71 | map.set(type, gqltype); 72 | return gqltype; 73 | } 74 | function createGQL(type: AllTypes, isInput: boolean): GraphQLType { 75 | const gqlType = map.get(type); 76 | if (gqlType) return gqlType; 77 | switch (type.kind) { 78 | case 'interface': 79 | case 'interfaceLiteral': 80 | return createGQLType(type, isInput); 81 | // case 'enum': 82 | // return add(type, createGQLEnum(type)); 83 | case 'union': 84 | case 'unionLiteral': 85 | if (isInput) return add(type, createGQLInputUnion(type)); 86 | else if (type.members.every(member => member.kind === 'primitive' && member.type === 'string')) return GraphQLString; 87 | else if (type.members.every(member => member.kind === 'primitive' && member.type === 'number' && member.rawType === 'Int')) return GraphQLInt; 88 | else if (type.members.every(member => member.kind === 'primitive' && member.type === 'number')) return GraphQLFloat; 89 | else if (type.members.every(member => member.kind === 'primitive')) throw new Error('Union primitives are not supported'); 90 | return add(type, createGQLUnion(type)); 91 | case 'array': 92 | return new GraphQLList(add(type, nullable(false, createGQL(type.element, isInput)))); 93 | case 'native': 94 | if (type.name === 'Date') { 95 | return nonNull(DateType); 96 | } 97 | throw new Error('Unexpected type: ' + type.name); 98 | case 'primitive': 99 | return add(type, createGQLPrimitive(type)); 100 | } 101 | throw new Error('Unexpected type: ' + JSON.stringify(type)); 102 | } 103 | 104 | function nullable(nullable: boolean, type: GraphQLType) { 105 | return nullable || type instanceof GraphQLNonNull ? type : new GraphQLNonNull(type); 106 | } 107 | 108 | function createGQLType(type: Interface | InterfaceLiteral, isInput: boolean): GraphQLType { 109 | let typeName = type.kind === 'interface' ? type.name : ''; 110 | const Class = isInput ? (GraphQLInputObjectType as unknown as typeof GraphQLObjectType) : GraphQLObjectType; 111 | 112 | const fields = {} as GraphQLFieldConfigMap<{}, {}>; 113 | if (type.kind === 'interfaceLiteral') { 114 | for (let i = 0; i < type.members.length; i++) { 115 | const member = type.members[i]; 116 | if ( 117 | member.name === '__typename' && 118 | member.type.kind === 'primitive' && 119 | typeof member.type.literal === 'string' 120 | ) { 121 | typeName = member.type.literal; 122 | } 123 | } 124 | } 125 | if (typeName === '') typeName = 'Anonymous' + (isInput ? 'Input' : '') + ++anonTypeIdx; 126 | const gqlType = new Class({ 127 | name: typeName, 128 | description: type.kind === 'interface' ? type.doc : undefined, 129 | fields: fields, 130 | }); 131 | add(type, gqlType); 132 | type.members.reduce((obj, member) => { 133 | // if (member.orUndefined) throw new Error('Undefined props are not supported in graphql'); 134 | const memberType = { 135 | type: nullable(member.orNull||member.orUndefined, createGQL(member.type, false)) as GraphQLOutputType, 136 | args: 137 | member.args && member.args.length === 1 138 | ? (member.args[0].type as InterfaceLiteral).members.reduce( 139 | (acc, arg) => { 140 | acc[arg.name] = { 141 | description: arg.doc, 142 | defaultValue: undefined, 143 | type: nullable(arg.orNull, createGQL(arg.type, true)) as GraphQLInputType, 144 | }; 145 | return acc; 146 | }, 147 | {} as GraphQLFieldConfigArgumentMap, 148 | ) 149 | : undefined, 150 | // todo: 151 | deprecationReason: undefined, 152 | description: member.doc, 153 | }; 154 | if (member.name !== '__typename') { 155 | obj[member.name] = memberType; 156 | } 157 | return obj; 158 | }, fields); 159 | return gqlType; 160 | } 161 | function createGQLUnion(type: Union | UnionLiteral): GraphQLType { 162 | return new GraphQLUnionType({ 163 | name: type.kind === 'union' ? type.name : 'AnonymousUnion' + ++anonTypeIdx, 164 | description: type.kind === 'union' ? type.doc : undefined, 165 | types: type.members.map(member => createGQL(member, false) as GraphQLObjectType), 166 | }); 167 | } 168 | function createGQLInputUnion(type: Union | UnionLiteral): GraphQLType { 169 | if (!type.members.every(m => m.kind === 'primitive' && m.type === 'string')) 170 | throw new Error('Input union supports only string unions'); 171 | const union = type.members.map(m => m.kind === 'primitive' && m.literal); 172 | const validate = (val: string) => { 173 | if (!union.includes(val)) 174 | throw new GraphQLError(`Input union: "${union.join(' | ')}" doesn't have value: ${val}`); 175 | return val; 176 | }; 177 | return new GraphQLScalarType({ 178 | name: type.kind === 'union' ? type.name : union.map(u => String(u).replace(/[^a-z]+/gi, '_')).join('__'), 179 | description: type.kind === 'union' ? type.doc : undefined, 180 | serialize: validate, 181 | parseValue: validate, 182 | parseLiteral(ast) { 183 | if (ast.kind === Kind.STRING) { 184 | return validate(ast.value); 185 | } 186 | return null; 187 | }, 188 | }); 189 | } 190 | function createGQLPrimitive(type: Primitive): GraphQLType { 191 | if (type.rawType === 'ID') return GraphQLID; 192 | const customType = customScalarsMap.get(type.type); 193 | if (customType) return customType; 194 | if (customScalar) { 195 | const res = customScalar(type); 196 | if (res) return res; 197 | } 198 | switch (type.type) { 199 | case 'number': 200 | return type.rawType === 'Int' ? GraphQLInt : GraphQLFloat; 201 | case 'string': 202 | return GraphQLString; 203 | case 'boolean': 204 | return GraphQLBoolean; 205 | } 206 | throw new Error('Unexpected type: ' + JSON.stringify(type)); 207 | } 208 | } 209 | 210 | function never(never: never): never { 211 | throw new Error('Never possible'); 212 | } 213 | function nonNull(val: T | undefined): T { 214 | if (val === undefined) throw new Error('Undefined is not expected here'); 215 | return val; 216 | } 217 | -------------------------------------------------------------------------------- /src/date.ts: -------------------------------------------------------------------------------- 1 | import {GraphQLScalarType, Kind, GraphQLError} from 'graphql'; 2 | 3 | export const DateType = new GraphQLScalarType({ 4 | name: 'Date', 5 | serialize: date => { 6 | if (Number.isNaN(date.getTime())) { 7 | throw new Error('Invalid response date'); 8 | } 9 | return date.toJSON(); 10 | }, 11 | parseValue: val => { 12 | return parse(val); 13 | }, 14 | parseLiteral(ast) { 15 | if (ast.kind === Kind.STRING) { 16 | return parse(ast.value); 17 | } 18 | return null; 19 | }, 20 | }); 21 | 22 | function parse(val: string) { 23 | const date = new Date(val); 24 | if (val.length !== 24 || Number.isNaN(date.getTime())) { 25 | throw new GraphQLError('Incorrect Date: ' + val); 26 | } 27 | return date; 28 | } 29 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { createSchema } from './createSchema'; 2 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | type Resolver = { [P in keyof T]: TypeSwitch }; 2 | type TypeSwitch = T extends (args: infer Args) => infer Ret 3 | ? (ctx?: never, args?: Args) => Promise> 4 | : Resolver | (() => Promise>); 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "commonjs", 5 | "jsx": "react", 6 | "outDir": "dist", 7 | "declaration": true, 8 | "strict": true 9 | } 10 | } 11 | --------------------------------------------------------------------------------