From 238adafeb0994bae574ac139bfb0b1e6f4614547 Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Fri, 2 Nov 2018 20:25:34 +0100 Subject: [PATCH 01/20] Started implementing enum support --- packages/graphqlgen/src/generators/common.ts | 18 ++++++-- .../src/generators/flow-generator.ts | 3 ++ .../graphqlgen/src/generators/ts-generator.ts | 6 ++- packages/graphqlgen/src/parse.ts | 8 +++- packages/graphqlgen/src/source-helper.ts | 44 +++++++++++-------- packages/graphqlgen/src/validation.ts | 4 +- 6 files changed, 55 insertions(+), 28 deletions(-) diff --git a/packages/graphqlgen/src/generators/common.ts b/packages/graphqlgen/src/generators/common.ts index f9a5ab92..f40e2152 100644 --- a/packages/graphqlgen/src/generators/common.ts +++ b/packages/graphqlgen/src/generators/common.ts @@ -5,7 +5,7 @@ import { GraphQLType, GraphQLTypeField, } from '../source-helper' -import { Model, ModelMap, ContextDefinition } from '../types' +import { Model, ModelMap, ContextDefinition, GenerateArgs } from '../types' import { ModelField } from '../introspection/ts-ast' import { flatten, uniq } from '../utils' @@ -94,10 +94,10 @@ export function getModelName(type: GraphQLType, modelMap: ModelMap): string { } function shouldRenderDefaultResolver( - type: GraphQLTypeObject, + graphQLType: GraphQLTypeObject, modelField: ModelField, ) { - const graphQLField = type.fields.find( + const graphQLField = graphQLType.fields.find( field => field.name === modelField.fieldName, ) @@ -133,7 +133,7 @@ export function printFieldLikeType( }${!field.type.isRequired ? '| null' : ''}` } - if (field.type.isInput) { + if (field.type.isInput || field.type.isEnum) { return `${field.type.name}${field.type.isArray ? '[]' : ''}${ !field.type.isRequired ? '| null' : '' }` @@ -189,3 +189,13 @@ export function getDistinctInputTypes( .reduce(flatten, []) .filter(uniq) } + +export function renderEnums(args: GenerateArgs): string { + return args.enums + .map(enumObject => { + return `type ${enumObject.name} = ${enumObject.values + .map(value => `'${value}'`) + .join(' | ')}` + }) + .join(os.EOL) +} diff --git a/packages/graphqlgen/src/generators/flow-generator.ts b/packages/graphqlgen/src/generators/flow-generator.ts index 228ede8f..aeaaf595 100644 --- a/packages/graphqlgen/src/generators/flow-generator.ts +++ b/packages/graphqlgen/src/generators/flow-generator.ts @@ -13,6 +13,7 @@ import { InputTypesMap, printFieldLikeType, getDistinctInputTypes, + renderEnums, } from './common' export function format(code: string, options: prettier.Options = {}) { @@ -66,6 +67,8 @@ export function generate(args: GenerateArgs): string { return `\ ${renderHeader(args)} + + ${renderEnums(args)} ${renderNamespaces(args, typeToInputTypeAssociation, inputTypesMap)} diff --git a/packages/graphqlgen/src/generators/ts-generator.ts b/packages/graphqlgen/src/generators/ts-generator.ts index 4f512afc..70de74b0 100644 --- a/packages/graphqlgen/src/generators/ts-generator.ts +++ b/packages/graphqlgen/src/generators/ts-generator.ts @@ -13,6 +13,7 @@ import { InputTypesMap, printFieldLikeType, getDistinctInputTypes, + renderEnums, } from './common' export function format(code: string, options: prettier.Options = {}) { @@ -66,6 +67,8 @@ export function generate(args: GenerateArgs): string { return `\ ${renderHeader(args)} + + ${renderEnums(args)} ${renderNamespaces(args, typeToInputTypeAssociation, inputTypesMap)} @@ -169,8 +172,7 @@ function renderInputTypeInterfaces( .map(typeAssociation => { return `export interface ${inputTypesMap[typeAssociation].name} { ${inputTypesMap[typeAssociation].fields.map( - field => - `${field.name}: ${printFieldLikeType(field, modelMap)}`, + field => `${field.name}: ${printFieldLikeType(field, modelMap)}`, )} }` }) diff --git a/packages/graphqlgen/src/parse.ts b/packages/graphqlgen/src/parse.ts index fd04bbab..a64714b5 100644 --- a/packages/graphqlgen/src/parse.ts +++ b/packages/graphqlgen/src/parse.ts @@ -18,7 +18,11 @@ import { getImportPathRelativeToOutput, } from './path-helpers' import { getTypeToFileMapping, replaceAll, normalizeFilePath } from './utils' -import { extractTypes, extractGraphQLTypesWithoutRootsAndInputs, GraphQLTypes } from './source-helper' +import { + extractTypes, + extractGraphQLTypesWithoutRootsAndInputsAndEnums, + GraphQLTypes, +} from './source-helper' const ajv = new Ajv().addMetaSchema( require('ajv/lib/refs/json-schema-draft-06.json'), @@ -140,7 +144,7 @@ export function parseModels( outputDir: string, language: Language, ): ModelMap { - const graphQLTypes = extractGraphQLTypesWithoutRootsAndInputs(schema) + const graphQLTypes = extractGraphQLTypesWithoutRootsAndInputsAndEnums(schema) const filePaths = !!models.files ? models.files.map(file => ({ defaultName: typeof file === 'object' ? file.defaultName : undefined, diff --git a/packages/graphqlgen/src/source-helper.ts b/packages/graphqlgen/src/source-helper.ts index 60b65325..f9374f06 100644 --- a/packages/graphqlgen/src/source-helper.ts +++ b/packages/graphqlgen/src/source-helper.ts @@ -275,23 +275,28 @@ function extractGraphQLTypes(schema: GraphQLSchema) { function extractGraphQLEnums(schema: GraphQLSchema) { const types: GraphQLEnumObject[] = [] - Object.values(schema.getTypeMap()).forEach((node: GraphQLNamedType) => { - if (node instanceof GraphQLEnumType) { - types.push({ - name: node.name, - type: { + Object.values(schema.getTypeMap()) + .filter( + (node: GraphQLNamedType) => + node.name !== '__TypeKind' && node.name !== '__DirectiveLocation', + ) + .forEach((node: GraphQLNamedType) => { + if (node instanceof GraphQLEnumType) { + types.push({ name: node.name, - isObject: false, - isInput: false, - isEnum: true, - isUnion: false, - isScalar: false, - isInterface: false, - }, - values: node.getValues().map(v => v.name), - }) - } - }) + type: { + name: node.name, + isObject: false, + isInput: false, + isEnum: true, + isUnion: false, + isScalar: false, + isInterface: false, + }, + values: node.getValues().map(v => v.name), + }) + } + }) return types } @@ -329,7 +334,9 @@ const graphqlToTypescriptFlow: { [key: string]: string } = { } export function graphQLToTypecriptFlowType(type: GraphQLType): string { - let typescriptType = type.isScalar ? graphqlToTypescriptFlow[type.name] : 'any' + let typescriptType = type.isScalar + ? graphqlToTypescriptFlow[type.name] + : 'any' if (type.isArray) { typescriptType += '[]' } @@ -339,11 +346,12 @@ export function graphQLToTypecriptFlowType(type: GraphQLType): string { return typescriptType } -export function extractGraphQLTypesWithoutRootsAndInputs( +export function extractGraphQLTypesWithoutRootsAndInputsAndEnums( schema: GraphQLTypes, ): GraphQLTypeObject[] { return schema.types .filter(type => !type.type.isInput) + .filter(type => !type.type.isEnum) .filter( type => ['Query', 'Mutation', 'Subscription'].indexOf(type.name) === -1, ) diff --git a/packages/graphqlgen/src/validation.ts b/packages/graphqlgen/src/validation.ts index 93c945ba..ebbd84b8 100644 --- a/packages/graphqlgen/src/validation.ts +++ b/packages/graphqlgen/src/validation.ts @@ -15,7 +15,7 @@ import { outputWrongSyntaxFiles, } from './output' import { - extractGraphQLTypesWithoutRootsAndInputs, + extractGraphQLTypesWithoutRootsAndInputsAndEnums, GraphQLTypes, } from './source-helper' import { normalizeFilePath, getTypeToFileMapping } from './utils' @@ -177,7 +177,7 @@ function validateSchemaToModelMapping( files: File[], language: Language, ): boolean { - const graphQLTypes = extractGraphQLTypesWithoutRootsAndInputs(schema) + const graphQLTypes = extractGraphQLTypesWithoutRootsAndInputsAndEnums(schema) const overridenTypeNames = validatedOverriddenModels.map( def => def.definition.typeName, ) From 440bd40d112ccf23cb0321f1fae27952114a39e6 Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Sun, 4 Nov 2018 17:41:15 +0100 Subject: [PATCH 02/20] Refactored ts introspection to truly parse model files --- .../src/generated/graphqlgen.ts | 6 +- packages/graphqlgen/src/generators/common.ts | 59 ++- .../src/generators/flow-generator.ts | 13 +- .../graphqlgen/src/generators/ts-generator.ts | 40 +- .../src/generators/ts-scaffolder.ts | 5 +- packages/graphqlgen/src/glob.ts | 4 +- packages/graphqlgen/src/index.ts | 21 +- .../graphqlgen/src/introspection/flow-ast.ts | 4 +- .../graphqlgen/src/introspection/ts-ast.ts | 420 +++++++++++++++--- packages/graphqlgen/src/parse.ts | 71 ++- .../src/tests/fixtures/context/flow-types.js | 6 +- .../src/tests/fixtures/context/types.ts | 2 +- .../src/tests/fixtures/prisma/types.ts | 4 +- .../flow/__snapshots__/basic.test.ts.snap | 1 - .../__snapshots__/large-schema.test.ts.snap | 4 - .../src/tests/introspection/flow.test.ts | 42 +- .../tests/introspection/typescript.test.ts | 42 +- .../__snapshots__/basic.test.ts.snap | 1 - .../__snapshots__/large-schema.test.ts.snap | 4 - packages/graphqlgen/src/types.ts | 3 +- packages/graphqlgen/src/utils.ts | 41 +- packages/graphqlgen/src/validation.ts | 68 ++- 22 files changed, 610 insertions(+), 251 deletions(-) diff --git a/packages/graphqlgen-templates/typescript-yoga/src/generated/graphqlgen.ts b/packages/graphqlgen-templates/typescript-yoga/src/generated/graphqlgen.ts index 1609abeb..8e7b2da3 100644 --- a/packages/graphqlgen-templates/typescript-yoga/src/generated/graphqlgen.ts +++ b/packages/graphqlgen-templates/typescript-yoga/src/generated/graphqlgen.ts @@ -1,10 +1,11 @@ // Code generated by github.com/prisma/graphqlgen, DO NOT EDIT. import { GraphQLResolveInfo } from 'graphql' -import { Context } from '../types' import { Post } from '../types' import { User } from '../types' +import { Context } from '../types' + export namespace QueryResolvers { export const defaultResolvers = {} @@ -141,8 +142,7 @@ export namespace PostResolvers { export namespace UserResolvers { export const defaultResolvers = { id: (parent: User) => parent.id, - name: (parent: User) => - parent.name === undefined || parent.name === null ? null : parent.name, + name: (parent: User) => (parent.name === undefined ? null : parent.name), } export type IdResolver = ( diff --git a/packages/graphqlgen/src/generators/common.ts b/packages/graphqlgen/src/generators/common.ts index f40e2152..b5ad1e41 100644 --- a/packages/graphqlgen/src/generators/common.ts +++ b/packages/graphqlgen/src/generators/common.ts @@ -5,8 +5,14 @@ import { GraphQLType, GraphQLTypeField, } from '../source-helper' -import { Model, ModelMap, ContextDefinition, GenerateArgs } from '../types' -import { ModelField } from '../introspection/ts-ast' +import { ModelMap, ContextDefinition, GenerateArgs } from '../types' +import { + ModelField, + Types, + InterfaceDefinition, + TypeAliasDefinition, + AnonymousInterfaceAnnotation, +} from '../introspection/ts-ast' import { flatten, uniq } from '../utils' type SpecificGraphQLScalarType = 'boolean' | 'number' | 'string' @@ -19,28 +25,61 @@ export interface TypeToInputTypeAssociation { [objectTypeName: string]: string[] } +export function fieldsFromModelDefinition(modelDef: Types): ModelField[] { + // If model is of type `interface InterfaceName { ... }` + if (modelDef.kind === 'InterfaceDefinition') { + const interfaceDef = modelDef as InterfaceDefinition + + return interfaceDef.fields.map(field => { + return { + fieldName: field.name, + fieldOptional: field.optional, + } + }) + } + // If model is of type `type TypeName = { ... }` + if ( + modelDef.kind === 'TypeAliasDefinition' && + (modelDef as TypeAliasDefinition).type.kind === + 'AnonymousInterfaceAnnotation' + ) { + const interfaceDef = (modelDef as TypeAliasDefinition) + .type as AnonymousInterfaceAnnotation + + return interfaceDef.fields.map(field => { + return { + fieldName: field.name, + fieldOptional: field.optional, + } + }) + } + + return [] +} + export function renderDefaultResolvers( - type: GraphQLTypeObject, + graphQLTypeObject: GraphQLTypeObject, modelMap: ModelMap, - extractFieldsFromModel: (model: Model) => ModelField[], variableName: string, ): string { - const model = modelMap[type.name] + const model = modelMap[graphQLTypeObject.name] if (model === undefined) { return `export const ${variableName} = {}` } - const modelFields = extractFieldsFromModel(model) + const modelDef = model.definition return `export const ${variableName} = { - ${modelFields - .filter(modelField => shouldRenderDefaultResolver(type, modelField)) + ${fieldsFromModelDefinition(modelDef) + .filter(modelField => + shouldRenderDefaultResolver(graphQLTypeObject, modelField), + ) .map(modelField => renderDefaultResolver( modelField.fieldName, modelField.fieldOptional, - model.modelTypeName, + model.definition.name, ), ) .join(os.EOL)} @@ -90,7 +129,7 @@ export function getModelName(type: GraphQLType, modelMap: ModelMap): string { return '{}' } - return model.modelTypeName + return model.definition.name } function shouldRenderDefaultResolver( diff --git a/packages/graphqlgen/src/generators/flow-generator.ts b/packages/graphqlgen/src/generators/flow-generator.ts index aeaaf595..2f43bd12 100644 --- a/packages/graphqlgen/src/generators/flow-generator.ts +++ b/packages/graphqlgen/src/generators/flow-generator.ts @@ -3,7 +3,6 @@ import * as prettier from 'prettier' import { GenerateArgs, ModelMap, ContextDefinition } from '../types' import { GraphQLTypeField, GraphQLTypeObject } from '../source-helper' -import { extractFieldsFromFlowType } from '../introspection/flow-ast' import { upperFirst } from '../utils' import { renderDefaultResolvers, @@ -13,7 +12,6 @@ import { InputTypesMap, printFieldLikeType, getDistinctInputTypes, - renderEnums, } from './common' export function format(code: string, options: prettier.Options = {}) { @@ -67,8 +65,6 @@ export function generate(args: GenerateArgs): string { return `\ ${renderHeader(args)} - - ${renderEnums(args)} ${renderNamespaces(args, typeToInputTypeAssociation, inputTypesMap)} @@ -82,7 +78,7 @@ function renderHeader(args: GenerateArgs): string { const modelImports = modelArray .map( m => - `import type { ${m.modelTypeName} } from '${ + `import type { ${m.definition.name} } from '${ m.importPathRelativeToOutput }'`, ) @@ -138,12 +134,7 @@ function renderNamespace( return `\ // Types for ${typeName} - ${renderDefaultResolvers( - type, - modelMap, - extractFieldsFromFlowType, - `${typeName}_defaultResolvers`, - )} + ${renderDefaultResolvers(type, modelMap, `${typeName}_defaultResolvers`)} ${renderInputTypeInterfaces( type, diff --git a/packages/graphqlgen/src/generators/ts-generator.ts b/packages/graphqlgen/src/generators/ts-generator.ts index 70de74b0..ea956e99 100644 --- a/packages/graphqlgen/src/generators/ts-generator.ts +++ b/packages/graphqlgen/src/generators/ts-generator.ts @@ -3,7 +3,7 @@ import * as prettier from 'prettier' import { GenerateArgs, ModelMap, ContextDefinition } from '../types' import { GraphQLTypeField, GraphQLTypeObject } from '../source-helper' -import { extractFieldsFromTypescriptType } from '../introspection/ts-ast' +import { TypeAliasDefinition } from '../introspection/ts-ast' import { upperFirst } from '../utils' import { renderDefaultResolvers, @@ -13,7 +13,6 @@ import { InputTypesMap, printFieldLikeType, getDistinctInputTypes, - renderEnums, } from './common' export function format(code: string, options: prettier.Options = {}) { @@ -67,8 +66,6 @@ export function generate(args: GenerateArgs): string { return `\ ${renderHeader(args)} - - ${renderEnums(args)} ${renderNamespaces(args, typeToInputTypeAssociation, inputTypesMap)} @@ -78,11 +75,23 @@ export function generate(args: GenerateArgs): string { } function renderHeader(args: GenerateArgs): string { - const modelArray = Object.keys(args.modelMap).map(k => args.modelMap[k]) + const modelArray = Object.keys(args.modelMap) + .filter(modelName => { + const modelDef = args.modelMap[modelName].definition + + return !( + modelDef.kind === 'TypeAliasDefinition' && + (modelDef as TypeAliasDefinition).isEnum + ) + }) + .map(modelName => args.modelMap[modelName]) + const modelImports = modelArray .map( m => - `import { ${m.modelTypeName} } from '${m.importPathRelativeToOutput}'`, + `import { ${m.definition.name} } from '${ + m.importPathRelativeToOutput + }'`, ) .join(os.EOL) @@ -124,34 +133,29 @@ function renderNamespaces( } function renderNamespace( - type: GraphQLTypeObject, + graphQLTypeObject: GraphQLTypeObject, typeToInputTypeAssociation: TypeToInputTypeAssociation, inputTypesMap: InputTypesMap, modelMap: ModelMap, context?: ContextDefinition, ): string { return `\ - export namespace ${type.name}Resolvers { + export namespace ${graphQLTypeObject.name}Resolvers { - ${renderDefaultResolvers( - type, - modelMap, - extractFieldsFromTypescriptType, - 'defaultResolvers', - )} + ${renderDefaultResolvers(graphQLTypeObject, modelMap, 'defaultResolvers')} ${renderInputTypeInterfaces( - type, + graphQLTypeObject, modelMap, typeToInputTypeAssociation, inputTypesMap, )} - ${renderInputArgInterfaces(type, modelMap)} + ${renderInputArgInterfaces(graphQLTypeObject, modelMap)} - ${renderResolverFunctionInterfaces(type, modelMap, context)} + ${renderResolverFunctionInterfaces(graphQLTypeObject, modelMap, context)} - ${renderResolverTypeInterface(type, modelMap, context)} + ${renderResolverTypeInterface(graphQLTypeObject, modelMap, context)} ${/* TODO renderResolverClass(type, modelMap) */ ''} } diff --git a/packages/graphqlgen/src/generators/ts-scaffolder.ts b/packages/graphqlgen/src/generators/ts-scaffolder.ts index 572101c2..06c92614 100644 --- a/packages/graphqlgen/src/generators/ts-scaffolder.ts +++ b/packages/graphqlgen/src/generators/ts-scaffolder.ts @@ -1,7 +1,6 @@ import { GenerateArgs, CodeFileLike, ModelMap } from '../types' import { GraphQLTypeField, GraphQLTypeObject } from '../source-helper' -import { extractFieldsFromTypescriptType } from '../introspection/ts-ast' -import { shouldScaffoldFieldResolver } from './common' +import { fieldsFromModelDefinition, shouldScaffoldFieldResolver } from "./common"; export { format } from './ts-generator' @@ -48,7 +47,7 @@ function renderResolvers( modelMap: ModelMap, ): CodeFileLike { const model = modelMap[type.name] - const modelFields = extractFieldsFromTypescriptType(model) + const modelFields = fieldsFromModelDefinition(model.definition) const code = `\ // This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. diff --git a/packages/graphqlgen/src/glob.ts b/packages/graphqlgen/src/glob.ts index abdb53ea..a19c5a71 100644 --- a/packages/graphqlgen/src/glob.ts +++ b/packages/graphqlgen/src/glob.ts @@ -21,7 +21,7 @@ export const extractGlobPattern = (paths?: string[]) => { /** * Handles the glob pattern of models.files */ -export const handleGlobPattern = (files?: File[]) => { +export const handleGlobPattern = (files?: File[]): File[] => { try { const newFiles: File[] = [] @@ -38,5 +38,7 @@ export const handleGlobPattern = (files?: File[]) => { return newFiles } catch (error) { console.log(error) + process.exit(1) + return [] } } diff --git a/packages/graphqlgen/src/index.ts b/packages/graphqlgen/src/index.ts index 8d58d7a0..c98a4069 100644 --- a/packages/graphqlgen/src/index.ts +++ b/packages/graphqlgen/src/index.ts @@ -221,15 +221,6 @@ resolver-scaffolding: } async function run() { - //TODO: Define proper defaults - // const defaults: DefaultOptions = { - // outputInterfaces: 'src/generated/resolvers.ts', - // outputScaffold: 'src/resolvers/', - // language: 'typescript', - // interfaces: '../generated/resolvers.ts', - // force: false, - // } - const argv = yargs .usage('Usage: graphqlgen or gg') .alias('i', 'init') @@ -249,11 +240,6 @@ async function run() { const config = parseConfig() const parsedSchema = parseSchema(config.schema) - const options = (await prettier.resolveConfig(process.cwd())) || {} // TODO: Abstract this TS specific behavior better - if (JSON.stringify(options) !== '{}') { - console.log(chalk.blue(`Found a prettier configuration to use`)) - } - // Override the config.models.files using handleGlobPattern config.models = { ...config.models, @@ -264,7 +250,6 @@ async function run() { return false } - //TODO: Should we provide a default in case `config.output.types` is not defined? const modelMap = parseModels( config.models, parsedSchema, @@ -272,6 +257,12 @@ async function run() { config.language, ) + const options = (await prettier.resolveConfig(process.cwd())) || {} // TODO: Abstract this TS specific behavior better + + if (JSON.stringify(options) !== '{}') { + console.log(chalk.blue(`Found a prettier configuration to use`)) + } + const { generatedTypes, generatedResolvers } = generateCode({ schema: parsedSchema!, language: config.language, diff --git a/packages/graphqlgen/src/introspection/flow-ast.ts b/packages/graphqlgen/src/introspection/flow-ast.ts index 2a5221f4..70e9affc 100644 --- a/packages/graphqlgen/src/introspection/flow-ast.ts +++ b/packages/graphqlgen/src/introspection/flow-ast.ts @@ -86,10 +86,10 @@ function isFieldOptional(node: ObjectTypeProperty) { export function extractFieldsFromFlowType(model: Model): ModelField[] { const filePath = model.absoluteFilePath - const typeNode = findFlowTypeByName(filePath, model.modelTypeName) + const typeNode = findFlowTypeByName(filePath, model.definition.name) if (!typeNode) { - throw new Error(`No interface found for name ${model.modelTypeName}`) + throw new Error(`No interface found for name ${model.definition}`) } const childrenNodes = diff --git a/packages/graphqlgen/src/introspection/ts-ast.ts b/packages/graphqlgen/src/introspection/ts-ast.ts index d2774425..7aafd51a 100644 --- a/packages/graphqlgen/src/introspection/ts-ast.ts +++ b/packages/graphqlgen/src/introspection/ts-ast.ts @@ -1,20 +1,104 @@ import * as fs from 'fs' import { File } from 'graphqlgen-json-schema' -import { getPath } from '../parse' -import { Model } from '../types' +import { buildFilesToTypesMap } from '../parse' import { parse as parseTS } from '@babel/parser' import { + ExportNamedDeclaration, File as TSFile, - TSTypeAliasDeclaration, - TSInterfaceDeclaration, + Identifier, + isTSArrayType, + isTSBooleanKeyword, + isTSLiteralType, + isTSNumberKeyword, + isTSPropertySignature, + isTSStringKeyword, + isTSTypeReference, + isTSUnionType, Statement, - ExportNamedDeclaration, - TSTypeLiteral, - TSInterfaceBody, + TSInterfaceDeclaration, TSPropertySignature, - Identifier, + TSType, + TSTypeAliasDeclaration, TSUnionType, + isTSAnyKeyword, + isTSNullKeyword, + isTSUndefinedKeyword, + isTSTypeLiteral, + TSTypeElement, + BaseNode, + isTSParenthesizedType, } from '@babel/types' +import chalk from "chalk"; + +type Type = + | TypeAnnotation + | UnionTypeAnnotation + | AnonymousInterfaceAnnotation + | LiteralTypeAnnotation + +type UnknownType = '_UNKNOWN_TYPE_' +type PrimitiveOrTypeRef = string | null | UnknownType + +// /!\ If you add a supported type of field, make sure you update isSupportedField() as well +type SupportedFields = TSPropertySignature + +interface UnionTypeAnnotation { + kind: 'UnionTypeAnnotation' + types: Type[] + isArray: boolean +} + +interface TypeAnnotation { + kind: 'TypeAnnotation' + type: PrimitiveOrTypeRef + isArray: boolean + isTypeRef: boolean +} + +export interface AnonymousInterfaceAnnotation { + kind: 'AnonymousInterfaceAnnotation' + fields: FieldDefinition[] + isArray: boolean +} + +interface LiteralTypeAnnotation { + kind: 'LiteralTypeAnnotation' + type: string + value: string | number | boolean + isArray: boolean +} + +type Kind = + | 'InterfaceDefinition' + | 'FieldDefinition' + | 'TypeAliasDefinition' + | 'TypeAliasValueDefinition' + +interface BaseTypeDefinition { + kind: Kind + name: string +} + +interface FieldDefinition { + name: string + type: Type + optional: boolean +} + +export interface InterfaceDefinition extends BaseTypeDefinition { + fields: FieldDefinition[] +} + +export interface TypeAliasDefinition extends BaseTypeDefinition { + type: Type + isEnum: boolean //If type is UnionType && `types` are scalar strings +} + +export type Types = InterfaceDefinition | TypeAliasDefinition + +export interface TypesMap { + [typeName: string]: Types +} export interface InterfaceNamesToFile { [interfaceName: string]: File @@ -27,10 +111,275 @@ export interface ModelField { type ExtractableType = TSTypeAliasDeclaration | TSInterfaceDeclaration -function getSourceFile(filePath: string): TSFile { +function createTypeAlias(name: string, type: Type): TypeAliasDefinition { + return { + kind: 'TypeAliasDefinition', + name, + type, + isEnum: + type.kind === 'UnionTypeAnnotation' && + type.types.every(unionType => { + return ( + unionType.kind === 'LiteralTypeAnnotation' && + unionType.isArray === false && + unionType.type === 'string' + ) + }), + } +} + +function createInterfaceField( + name: string, + type: Type, + optional: boolean, +): FieldDefinition { + return { + name, + type, + optional, + } +} + +function createInterface( + name: string, + fields: FieldDefinition[], +): InterfaceDefinition { + return { + kind: 'InterfaceDefinition', + name, + fields, + } +} + +interface TypeAnnotationOpts { + isArray?: boolean + isTypeRef?: boolean + isAny?: boolean +} + +function createTypeAnnotation( + type: PrimitiveOrTypeRef, + options?: TypeAnnotationOpts, +): TypeAnnotation { + let opts: TypeAnnotationOpts = {} + if (options === undefined) { + opts = { isArray: false, isTypeRef: false, isAny: false } + } else { + opts = { + isArray: options.isArray === undefined ? false : options.isArray, + isTypeRef: options.isTypeRef === undefined ? false : options.isTypeRef, + isAny: options.isAny === undefined ? false : options.isAny, + } + } + + const isArray = opts.isArray === undefined ? false : opts.isArray + const isTypeRef = opts.isTypeRef === undefined ? false : opts.isTypeRef + + return { + kind: 'TypeAnnotation', + type, + isArray, + isTypeRef, + } +} + +function createUnionTypeAnnotation( + types: Type[], + isArray: boolean = false, +): UnionTypeAnnotation { + return { + kind: 'UnionTypeAnnotation', + types, + isArray, + } +} + +function createAnonymousInterfaceAnnotation( + fields: FieldDefinition[], + isArray: boolean = false, +): AnonymousInterfaceAnnotation { + return { + kind: 'AnonymousInterfaceAnnotation', + fields, + isArray, + } +} + +function createLiteralTypeAnnotation( + type: string, + value: string | number | boolean, + isArray: boolean = false, +): LiteralTypeAnnotation { + return { + kind: 'LiteralTypeAnnotation', + type, + value, + isArray, + } +} + +function computeType(node: TSType, filePath: string): Type { + if (isTSParenthesizedType(node)) { + node = node.typeAnnotation + } + + if (isTSStringKeyword(node)) { + return createTypeAnnotation('string') + } + if (isTSNumberKeyword(node)) { + return createTypeAnnotation('number') + } + if (isTSBooleanKeyword(node)) { + return createTypeAnnotation('boolean') + } + if (isTSAnyKeyword(node)) { + return createTypeAnnotation(null, { isAny: true }) + } + if (isTSNullKeyword(node)) { + return createTypeAnnotation(null) + } + if (isTSNullKeyword(node) || isTSUndefinedKeyword(node)) { + return createTypeAnnotation(null) + } + if (isTSTypeReference(node)) { + const typeRefName = (node.typeName as Identifier).name + + return createTypeAnnotation(typeRefName, { isTypeRef: true }) + } + if (isTSArrayType(node)) { + const computedType = computeType(node.elementType, filePath) + + computedType.isArray = true + + return computedType + } + if (isTSLiteralType(node)) { + const literalValue = node.literal.value + + return createLiteralTypeAnnotation(typeof literalValue, literalValue) + } + + if (isTSTypeLiteral(node)) { + const fields = node.members as SupportedFields[] + const interfaceFields = extractInterfaceFields(fields, filePath) + + return createAnonymousInterfaceAnnotation(interfaceFields) + } + + if (isTSUnionType(node)) { + const unionTypes = node.types.map(unionType => computeType(unionType, filePath)) + + return createUnionTypeAnnotation(unionTypes) + } + + console.log( + chalk.yellow(`WARNING: Unsupported type ${node.type} (Line ${getLine( + node, + )} in ${filePath}). Please file an issue at https://github.com/prisma/graphqlgen/issues`), + ) + + return createTypeAnnotation('_UNKNOWN_TYPE_') +} + +function getLine(node: BaseNode) { + return node.loc === null ? 'unknown' : node.loc.start.line +} + +function extractTypeAlias( + typeName: string, + typeAlias: TSTypeAliasDeclaration, + filePath: string +): TypeAliasDefinition | InterfaceDefinition { + if (isTSTypeLiteral(typeAlias.typeAnnotation)) { + return extractInterface(typeName, typeAlias.typeAnnotation.members, filePath) + } else { + const typeAliasType = computeType(typeAlias.typeAnnotation, filePath) + + return createTypeAlias(typeName, typeAliasType) + } +} + +function isSupportedTypeOfField(field: TSTypeElement) { + return isTSPropertySignature(field) +} + +function throwIfUnsupportedFields(fields: TSTypeElement[], filePath: string) { + const unsupportedFields = fields.filter( + field => !isSupportedTypeOfField(field), + ) + + if (unsupportedFields.length > 0) { + throw new Error( + `Unsupported notation for fields: ${unsupportedFields + .map(field => `Line ${getLine(field)} in ${filePath}`) + .join(', ')}`, + ) + } +} + +function extractInterfaceFields(fields: TSTypeElement[], filePath: string) { + throwIfUnsupportedFields(fields, filePath) + + return (fields as SupportedFields[]).map(field => { + const fieldName = (field.key as Identifier).name + + if (!field.typeAnnotation) { + throw new Error(`ERROR: Unsupported notation (Line ${getLine(field)} in ${filePath})`) + } + + const fieldType = computeType(field.typeAnnotation!.typeAnnotation, filePath) + const isOptional = isFieldOptional(field) + + return createInterfaceField(fieldName, fieldType, isOptional) + }) +} + +function extractInterface( + typeName: string, + fields: TSTypeElement[], + filePath: string +): InterfaceDefinition { + const interfaceFields = extractInterfaceFields(fields, filePath) + + return createInterface(typeName, interfaceFields) +} + +const cachedFilesToTypeMaps: { [filePath: string]: TypesMap } = {} + +export function buildTypesMap(filePath: string): TypesMap { + if (cachedFilesToTypeMaps[filePath] !== undefined) { + return cachedFilesToTypeMaps[filePath] + } + const file = fs.readFileSync(filePath).toString() - return parseTS(file, { plugins: ['typescript'], sourceType: 'module' }) + const ast = parseTS(file, { + plugins: ['typescript'], + sourceType: 'module', + tokens: true, + }) + + const typesMap = findTypescriptTypes(ast).reduce( + (acc, type) => { + const typeName = type.id.name + if (type.type === 'TSTypeAliasDeclaration') { + return { + ...acc, + [typeName]: extractTypeAlias(typeName, type, filePath), + } + } + + return { + ...acc, + [typeName]: extractInterface(typeName, type.body.body, filePath), + } + }, + {} as TypesMap, + ) + + cachedFilesToTypeMaps[filePath] = typesMap + + return typesMap } function shouldExtractType(node: Statement) { @@ -40,7 +389,7 @@ function shouldExtractType(node: Statement) { ) } -function getTypescriptTypes(sourceFile: TSFile): ExtractableType[] { +function findTypescriptTypes(sourceFile: TSFile): ExtractableType[] { const statements = sourceFile.program.body const types = statements.filter(shouldExtractType) @@ -59,20 +408,13 @@ function getTypescriptTypes(sourceFile: TSFile): ExtractableType[] { return [...types, ...typesFromNamedExport] as ExtractableType[] } -export function findTypescriptInterfaceByName( +export function findTypeInFile( filePath: string, typeName: string, -): ExtractableType | undefined { - const sourceFile = getSourceFile(filePath) +): Types | undefined { + const filesToTypesMap = buildFilesToTypesMap([{ path: filePath }]) - return getTypescriptTypes(sourceFile).find(node => node.id.name === typeName) -} - -export function typeNamesFromTypescriptFile(file: File): string[] { - const filePath = getPath(file) - const sourceFile = getSourceFile(filePath) - - return getTypescriptTypes(sourceFile).map(node => node.id.name) + return filesToTypesMap[filePath][typeName] } function isFieldOptional(node: TSPropertySignature) { @@ -98,37 +440,3 @@ function isFieldOptional(node: TSPropertySignature) { return false } - -export function extractFieldsFromTypescriptType(model: Model): ModelField[] { - const filePath = model.absoluteFilePath - const typeNode = findTypescriptInterfaceByName(filePath, model.modelTypeName) - - if (!typeNode) { - throw new Error(`No interface found for name ${model.modelTypeName}`) - } - - if ( - typeNode.type === 'TSTypeAliasDeclaration' && - (typeNode as TSTypeAliasDeclaration).typeAnnotation.type !== 'TSTypeLiteral' - ) { - throw new Error( - `Type notation not supported for type ${model.modelTypeName}`, - ) - } - - const childrenNodes = - typeNode.type === 'TSTypeAliasDeclaration' - ? ((typeNode as TSTypeAliasDeclaration).typeAnnotation as TSTypeLiteral) - .members - : ((typeNode as TSInterfaceDeclaration).body as TSInterfaceBody).body - - return childrenNodes - .filter(childNode => childNode.type === 'TSPropertySignature') - .map(childNode => { - const childNodeProperty = childNode as TSPropertySignature - const fieldName = (childNodeProperty.key as Identifier).name - const fieldOptional = isFieldOptional(childNodeProperty) - - return { fieldName, fieldOptional } - }) -} diff --git a/packages/graphqlgen/src/parse.ts b/packages/graphqlgen/src/parse.ts index a64714b5..07851654 100644 --- a/packages/graphqlgen/src/parse.ts +++ b/packages/graphqlgen/src/parse.ts @@ -12,7 +12,7 @@ import { } from 'graphqlgen-json-schema' import schema = require('graphqlgen-json-schema/dist/schema.json') -import { ContextDefinition, ModelMap } from './types' +import { ContextDefinition, ModelMap, Model } from './types' import { getAbsoluteFilePath, getImportPathRelativeToOutput, @@ -23,6 +23,16 @@ import { extractGraphQLTypesWithoutRootsAndInputsAndEnums, GraphQLTypes, } from './source-helper' +import { buildTypesMap, TypesMap } from './introspection/ts-ast' + +export interface FilesToTypesMap { + [filePath: string]: TypesMap +} + +export interface NormalizedFile { + path: string + defaultName?: string +} const ajv = new Ajv().addMetaSchema( require('ajv/lib/refs/json-schema-draft-06.json'), @@ -105,11 +115,12 @@ export function parseSchema(schemaPath: string): GraphQLTypes { } function buildModel( - filePath: string, modelName: string, + filePath: string, + filesToTypesMap: FilesToTypesMap, outputDir: string, language: Language, -) { +): Model { const absoluteFilePath = getAbsoluteFilePath(filePath, language) const importPathRelativeToOutput = getImportPathRelativeToOutput( absoluteFilePath, @@ -118,7 +129,7 @@ function buildModel( return { absoluteFilePath, importPathRelativeToOutput, - modelTypeName: modelName, + definition: filesToTypesMap[filePath][modelName], } } @@ -138,6 +149,29 @@ export function getDefaultName(file: File): string | null { return file.defaultName || null } +export function buildFilesToTypesMap( + files: NormalizedFile[], +): FilesToTypesMap { + return files.reduce((acc, file) => { + return { + ...acc, + [file.path]: buildTypesMap(file.path), + } + }, {}) +} + +export function normalizeFiles( + files: File[] | undefined, + language: Language, +): NormalizedFile[] { + return files !== undefined + ? files.map(file => ({ + defaultName: typeof file === 'object' ? file.defaultName : undefined, + path: normalizeFilePath(getPath(file), language), + })) + : [] +} + export function parseModels( models: Models, schema: GraphQLTypes, @@ -145,14 +179,13 @@ export function parseModels( language: Language, ): ModelMap { const graphQLTypes = extractGraphQLTypesWithoutRootsAndInputsAndEnums(schema) - const filePaths = !!models.files - ? models.files.map(file => ({ - defaultName: typeof file === 'object' ? file.defaultName : undefined, - path: normalizeFilePath(getPath(file), language), - })) - : [] + const normalizedFiles = normalizeFiles(models.files, language) + const filesToTypesMap = buildFilesToTypesMap(normalizedFiles) const overriddenModels = !!models.override ? models.override : {} - const typeToFileMapping = getTypeToFileMapping(filePaths, language) + const typeToFileMapping = getTypeToFileMapping( + normalizedFiles, + filesToTypesMap, + ) return graphQLTypes.reduce((acc, type) => { if (overriddenModels[type.name]) { @@ -160,7 +193,13 @@ export function parseModels( return { ...acc, - [type.name]: buildModel(filePath, modelName, outputDir, language), + [type.name]: buildModel( + modelName, + filePath, + filesToTypesMap, + outputDir, + language, + ), } } @@ -192,7 +231,13 @@ export function parseModels( return { ...acc, - [type.name]: buildModel(filePath, replacedTypeName, outputDir, language), + [type.name]: buildModel( + replacedTypeName, + filePath, + filesToTypesMap, + outputDir, + language, + ), } }, {}) } diff --git a/packages/graphqlgen/src/tests/fixtures/context/flow-types.js b/packages/graphqlgen/src/tests/fixtures/context/flow-types.js index 63a172ba..1c41e32d 100644 --- a/packages/graphqlgen/src/tests/fixtures/context/flow-types.js +++ b/packages/graphqlgen/src/tests/fixtures/context/flow-types.js @@ -1,13 +1,13 @@ // @flow interface Data { - users: [] + users: User[]; } interface Context { - data: Data + data: Data; } interface User { - id: string + id: string; } diff --git a/packages/graphqlgen/src/tests/fixtures/context/types.ts b/packages/graphqlgen/src/tests/fixtures/context/types.ts index fed1367a..afd5c4a3 100644 --- a/packages/graphqlgen/src/tests/fixtures/context/types.ts +++ b/packages/graphqlgen/src/tests/fixtures/context/types.ts @@ -1,5 +1,5 @@ interface Data { - users: [] + users: User[] } interface Context { diff --git a/packages/graphqlgen/src/tests/fixtures/prisma/types.ts b/packages/graphqlgen/src/tests/fixtures/prisma/types.ts index 7aa42591..0b05f204 100644 --- a/packages/graphqlgen/src/tests/fixtures/prisma/types.ts +++ b/packages/graphqlgen/src/tests/fixtures/prisma/types.ts @@ -1,4 +1,6 @@ -export interface Experience { +export type EnumType = 'RED' | 'GREEN' + +export type Experience = { id: string category: any | null title: string diff --git a/packages/graphqlgen/src/tests/flow/__snapshots__/basic.test.ts.snap b/packages/graphqlgen/src/tests/flow/__snapshots__/basic.test.ts.snap index 4c66ad33..cacdb9ff 100644 --- a/packages/graphqlgen/src/tests/flow/__snapshots__/basic.test.ts.snap +++ b/packages/graphqlgen/src/tests/flow/__snapshots__/basic.test.ts.snap @@ -5,7 +5,6 @@ exports[`basic enum 1`] = ` // Code generated by github.com/prisma/graphqlgen, DO NOT EDIT. import type { GraphQLResolveInfo } from \\"graphql\\"; -import type { UserType } from \\"../../../fixtures/enum/types-flow\\"; import type { User } from \\"../../../fixtures/enum/types-flow\\"; type Context = any; diff --git a/packages/graphqlgen/src/tests/flow/__snapshots__/large-schema.test.ts.snap b/packages/graphqlgen/src/tests/flow/__snapshots__/large-schema.test.ts.snap index 093b4a3c..06648af1 100644 --- a/packages/graphqlgen/src/tests/flow/__snapshots__/large-schema.test.ts.snap +++ b/packages/graphqlgen/src/tests/flow/__snapshots__/large-schema.test.ts.snap @@ -19,21 +19,17 @@ import type { Viewer } from \\"../../../fixtures/prisma/flow-types\\"; import type { User } from \\"../../../fixtures/prisma/flow-types\\"; import type { Booking } from \\"../../../fixtures/prisma/flow-types\\"; import type { Place } from \\"../../../fixtures/prisma/flow-types\\"; -import type { PLACE_SIZES } from \\"../../../fixtures/prisma/flow-types\\"; import type { Amenities } from \\"../../../fixtures/prisma/flow-types\\"; import type { Pricing } from \\"../../../fixtures/prisma/flow-types\\"; -import type { CURRENCY } from \\"../../../fixtures/prisma/flow-types\\"; import type { PlaceViews } from \\"../../../fixtures/prisma/flow-types\\"; import type { GuestRequirements } from \\"../../../fixtures/prisma/flow-types\\"; import type { Policies } from \\"../../../fixtures/prisma/flow-types\\"; import type { HouseRules } from \\"../../../fixtures/prisma/flow-types\\"; import type { Payment } from \\"../../../fixtures/prisma/flow-types\\"; import type { PaymentAccount } from \\"../../../fixtures/prisma/flow-types\\"; -import type { PAYMENT_PROVIDER } from \\"../../../fixtures/prisma/flow-types\\"; import type { PaypalInformation } from \\"../../../fixtures/prisma/flow-types\\"; import type { CreditCardInformation } from \\"../../../fixtures/prisma/flow-types\\"; import type { Notification } from \\"../../../fixtures/prisma/flow-types\\"; -import type { NOTIFICATION_TYPE } from \\"../../../fixtures/prisma/flow-types\\"; import type { Message } from \\"../../../fixtures/prisma/flow-types\\"; import type { AuthPayload } from \\"../../../fixtures/prisma/flow-types\\"; import type { MutationResult } from \\"../../../fixtures/prisma/flow-types\\"; diff --git a/packages/graphqlgen/src/tests/introspection/flow.test.ts b/packages/graphqlgen/src/tests/introspection/flow.test.ts index 32a45d25..cfd64dbf 100644 --- a/packages/graphqlgen/src/tests/introspection/flow.test.ts +++ b/packages/graphqlgen/src/tests/introspection/flow.test.ts @@ -1,28 +1,34 @@ -import { join } from "path"; -import { Model } from "../../types"; -import { typeNamesFromFlowFile, extractFieldsFromFlowType } from "../../introspection/flow-ast"; +import { join } from 'path' +import { typeNamesFromFlowFile } from '../../introspection/flow-ast' const relative = (p: string) => join(__dirname, p) describe('flow file introspection', () => { test('find all types in file', () => { - const typesNames = typeNamesFromFlowFile({ path: relative('./mocks/flow-types.js') }) + const typesNames = typeNamesFromFlowFile({ + path: relative('./mocks/flow-types.js'), + }) - expect(typesNames).toEqual(['Interface', 'Type', 'ExportedInterface', 'ExportedType']) + expect(typesNames).toEqual([ + 'Interface', + 'Type', + 'ExportedInterface', + 'ExportedType', + ]) }) - test('extract fields from flow type', () => { - const model: Model = { - modelTypeName: 'Interface', - absoluteFilePath: relative('./mocks/flow-types.js'), - importPathRelativeToOutput: 'not_used' - } - const fields = extractFieldsFromFlowType(model) + // test('extract fields from flow type', () => { + // const model: Model = { + // definition: 'Interface', + // absoluteFilePath: relative('./mocks/flow-types.js'), + // importPathRelativeToOutput: 'not_used' + // } + // const fields = extractFieldsFromFlowType(model) - expect(fields).toEqual([ - { fieldName: 'field', fieldOptional: false }, - { fieldName: 'optionalField', fieldOptional: true }, - { fieldName: 'fieldUnionNull', fieldOptional: true }, - ]) - }) + // expect(fields).toEqual([ + // { fieldName: 'field', fieldOptional: false }, + // { fieldName: 'optionalField', fieldOptional: true }, + // { fieldName: 'fieldUnionNull', fieldOptional: true }, + // ]) + // }) }) diff --git a/packages/graphqlgen/src/tests/introspection/typescript.test.ts b/packages/graphqlgen/src/tests/introspection/typescript.test.ts index 461cb2c5..b7773c21 100644 --- a/packages/graphqlgen/src/tests/introspection/typescript.test.ts +++ b/packages/graphqlgen/src/tests/introspection/typescript.test.ts @@ -1,31 +1,29 @@ -import { - extractFieldsFromTypescriptType, - typeNamesFromTypescriptFile -} from "../../introspection/ts-ast"; -import { join } from "path"; -import { Model } from "../../types"; +import { join } from 'path' +import { buildTypesMap } from '../../introspection/ts-ast' const relative = (p: string) => join(__dirname, p) describe('typescript file introspection', () => { test('find all types in file', () => { - const typesNames = typeNamesFromTypescriptFile({ path: relative('./mocks/types.ts') }) + const typesNames = Object.keys(buildTypesMap(relative('./mocks/types.ts'))) - expect(typesNames).toEqual(['Interface', 'Type', 'ExportedInterface', 'ExportedType']) - }) - - test('extract fields from typescript type', () => { - const model: Model = { - modelTypeName: 'Interface', - absoluteFilePath: relative('./mocks/types.ts'), - importPathRelativeToOutput: 'not_used' - } - const fields = extractFieldsFromTypescriptType(model) - - expect(fields).toEqual([ - { fieldName: 'field', fieldOptional: false }, - { fieldName: 'optionalField', fieldOptional: true }, - { fieldName: 'fieldUnionNull', fieldOptional: true }, + expect(typesNames).toEqual([ + 'Interface', + 'Type', + 'ExportedInterface', + 'ExportedType', ]) }) + + // TODO: Update test + // test('extract fields from typescript type', () => { + // const model: Model = { modelTypeName: 'Interface', absoluteFilePath: relative('./mocks/types.ts'), importPathRelativeToOutput: 'not_used' } + // const fields = extractFieldsFromTypescriptType(model) + // + // expect(fields).toEqual([ + // { fieldName: 'field', fieldOptional: false }, + // { fieldName: 'optionalField', fieldOptional: true }, + // { fieldName: 'fieldUnionNull', fieldOptional: true }, + // ]) + // }) }) diff --git a/packages/graphqlgen/src/tests/typescript/__snapshots__/basic.test.ts.snap b/packages/graphqlgen/src/tests/typescript/__snapshots__/basic.test.ts.snap index a2a0007a..cf3253ab 100644 --- a/packages/graphqlgen/src/tests/typescript/__snapshots__/basic.test.ts.snap +++ b/packages/graphqlgen/src/tests/typescript/__snapshots__/basic.test.ts.snap @@ -4,7 +4,6 @@ exports[`basic enum 1`] = ` "// Code generated by github.com/prisma/graphqlgen, DO NOT EDIT. import { GraphQLResolveInfo } from \\"graphql\\"; -import { UserType } from \\"../../../fixtures/enum/types\\"; import { User } from \\"../../../fixtures/enum/types\\"; type Context = any; diff --git a/packages/graphqlgen/src/tests/typescript/__snapshots__/large-schema.test.ts.snap b/packages/graphqlgen/src/tests/typescript/__snapshots__/large-schema.test.ts.snap index 3f07a55d..ae1091c9 100644 --- a/packages/graphqlgen/src/tests/typescript/__snapshots__/large-schema.test.ts.snap +++ b/packages/graphqlgen/src/tests/typescript/__snapshots__/large-schema.test.ts.snap @@ -18,21 +18,17 @@ import { Viewer } from \\"../../../fixtures/prisma/types\\"; import { User } from \\"../../../fixtures/prisma/types\\"; import { Booking } from \\"../../../fixtures/prisma/types\\"; import { Place } from \\"../../../fixtures/prisma/types\\"; -import { PLACE_SIZES } from \\"../../../fixtures/prisma/types\\"; import { Amenities } from \\"../../../fixtures/prisma/types\\"; import { Pricing } from \\"../../../fixtures/prisma/types\\"; -import { CURRENCY } from \\"../../../fixtures/prisma/types\\"; import { PlaceViews } from \\"../../../fixtures/prisma/types\\"; import { GuestRequirements } from \\"../../../fixtures/prisma/types\\"; import { Policies } from \\"../../../fixtures/prisma/types\\"; import { HouseRules } from \\"../../../fixtures/prisma/types\\"; import { Payment } from \\"../../../fixtures/prisma/types\\"; import { PaymentAccount } from \\"../../../fixtures/prisma/types\\"; -import { PAYMENT_PROVIDER } from \\"../../../fixtures/prisma/types\\"; import { PaypalInformation } from \\"../../../fixtures/prisma/types\\"; import { CreditCardInformation } from \\"../../../fixtures/prisma/types\\"; import { Notification } from \\"../../../fixtures/prisma/types\\"; -import { NOTIFICATION_TYPE } from \\"../../../fixtures/prisma/types\\"; import { Message } from \\"../../../fixtures/prisma/types\\"; import { AuthPayload } from \\"../../../fixtures/prisma/types\\"; import { MutationResult } from \\"../../../fixtures/prisma/types\\"; diff --git a/packages/graphqlgen/src/types.ts b/packages/graphqlgen/src/types.ts index 97a52253..21133c52 100644 --- a/packages/graphqlgen/src/types.ts +++ b/packages/graphqlgen/src/types.ts @@ -4,6 +4,7 @@ import { GraphQLEnumObject, GraphQLUnionObject, } from './source-helper' +import { Types } from './introspection/ts-ast' export interface GenerateArgs { types: GraphQLTypeObject[] @@ -20,7 +21,7 @@ export interface ModelMap { export interface Model { absoluteFilePath: string importPathRelativeToOutput: string - modelTypeName: string + definition: Types } export interface ContextDefinition { diff --git a/packages/graphqlgen/src/utils.ts b/packages/graphqlgen/src/utils.ts index 0db355a2..ce565ddd 100644 --- a/packages/graphqlgen/src/utils.ts +++ b/packages/graphqlgen/src/utils.ts @@ -1,9 +1,9 @@ import * as path from 'path' -import { Language, File } from 'graphqlgen-json-schema' +import { Language } from 'graphqlgen-json-schema' import { getExtNameFromLanguage } from './path-helpers' -import { InterfaceNamesToFile, typeNamesFromTypescriptFile } from './introspection/ts-ast' -import { typeNamesFromFlowFile } from './introspection/flow-ast' +import { InterfaceNamesToFile } from './introspection/ts-ast' +import { FilesToTypesMap, NormalizedFile } from './parse' export function upperFirst(s: string) { return s.replace(/^\w/, c => c.toUpperCase()) @@ -30,34 +30,29 @@ export function normalizeFilePath( return filePath } -function typeNamesFromFile(file: File, language: Language) { - switch (language) { - case 'typescript': - return typeNamesFromTypescriptFile(file) - case 'flow': - return typeNamesFromFlowFile(file) - } -} - /** * Create a map of interface names to the path of the file in which they're defined * The first evaluated interfaces are always the chosen ones */ export function getTypeToFileMapping( - files: File[], - language: Language, + files: NormalizedFile[], + filesToTypesMap: FilesToTypesMap, ): InterfaceNamesToFile { - return files.reduce((acc: InterfaceNamesToFile, file: File) => { - const interfaceNames = typeNamesFromFile(file, language).filter( - interfaceName => !acc[interfaceName], - ) + return files.reduce( + (acc, file) => { + const typesMap = filesToTypesMap[file.path] + const interfaceNames = Object.keys(typesMap).filter( + interfaceName => !acc[interfaceName], + ) - interfaceNames.forEach(interfaceName => { - acc[interfaceName] = file - }) + interfaceNames.forEach(interfaceName => { + acc[interfaceName] = file + }) - return acc - }, {}) + return acc + }, + {} as InterfaceNamesToFile, + ) } export function replaceAll(str: string, search: string, replacement: string) { diff --git a/packages/graphqlgen/src/validation.ts b/packages/graphqlgen/src/validation.ts index ebbd84b8..5810f7a0 100644 --- a/packages/graphqlgen/src/validation.ts +++ b/packages/graphqlgen/src/validation.ts @@ -1,12 +1,7 @@ import chalk from 'chalk' import { existsSync } from 'fs' -import { - GraphQLGenDefinition, - Language, - Models, - File, -} from 'graphqlgen-json-schema' -import { findTypescriptInterfaceByName } from './introspection/ts-ast' +import { GraphQLGenDefinition, Language, Models } from 'graphqlgen-json-schema' +import { findTypeInFile } from './introspection/ts-ast' import { outputDefinitionFilesNotFound, outputInterfaceDefinitionsNotFound, @@ -19,8 +14,14 @@ import { GraphQLTypes, } from './source-helper' import { normalizeFilePath, getTypeToFileMapping } from './utils' -import { replaceVariablesInString, getPath, getDefaultName } from './parse' -import { findFlowTypeByName } from './introspection/flow-ast' +import { + replaceVariablesInString, + getPath, + getDefaultName, + normalizeFiles, + buildFilesToTypesMap, + NormalizedFile, +} from './parse' type Definition = { typeName: string @@ -108,16 +109,14 @@ export function validateModels( schema: GraphQLTypes, language: Language, ): boolean { - const filePaths = !!models.files - ? models.files.map(file => ({ - defaultName: typeof file === 'object' ? file.defaultName : undefined, - path: normalizeFilePath(getPath(file), language), - })) - : [] + const normalizedFiles = normalizeFiles(models.files, language) const overriddenModels = !!models.override ? models.override : {} - // First test if all files are existing - if (filePaths.length > 0) { - const invalidFiles = filePaths.filter(file => !existsSync(getPath(file))) + + // Make sure all files exist + if (normalizedFiles.length > 0) { + const invalidFiles = normalizedFiles.filter( + file => !existsSync(getPath(file)), + ) if (invalidFiles.length > 0) { outputModelFilesNotFound(invalidFiles.map(f => f.path)) @@ -140,7 +139,7 @@ export function validateModels( return validateSchemaToModelMapping( schema, validatedOverriddenModels, - filePaths, + normalizedFiles, language, ) } @@ -174,14 +173,19 @@ function testValidatedDefinitions( function validateSchemaToModelMapping( schema: GraphQLTypes, validatedOverriddenModels: ValidatedDefinition[], - files: File[], + normalizedFiles: NormalizedFile[], language: Language, ): boolean { const graphQLTypes = extractGraphQLTypesWithoutRootsAndInputsAndEnums(schema) const overridenTypeNames = validatedOverriddenModels.map( def => def.definition.typeName, ) - const interfaceNamesToPath = getTypeToFileMapping(files, language) + + const filesToTypesMap = buildFilesToTypesMap(normalizedFiles) + const interfaceNamesToPath = getTypeToFileMapping( + normalizedFiles, + filesToTypesMap, + ) const missingModels = graphQLTypes.filter(type => { // If some overridden models are mapped to a GraphQL type, consider them valid @@ -205,8 +209,8 @@ function validateSchemaToModelMapping( // Append the user's chosen defaultName pattern to the step 1 missing models, // but only if they have the same pattern for all of their files let defaultName: string | null = null - if (files.length > 0) { - const names = files.map(getDefaultName) + if (normalizedFiles.length > 0) { + const names = normalizedFiles.map(getDefaultName) if (names.every(name => name === names[0])) { defaultName = names[0] } @@ -224,20 +228,6 @@ export function maybeReplaceDefaultName(typeName: string, defaultName?: string | : typeName } -// Check whether the model definition exists in typescript/flow file -function interfaceDefinitionExistsInFile( - filePath: string, - modelName: string, - language: Language, -): boolean { - switch (language) { - case 'typescript': - return findTypescriptInterfaceByName(filePath, modelName) !== undefined - case 'flow': - return findFlowTypeByName(filePath, modelName) !== undefined - } -} - export function validateDefinition( typeName: string, definition: string, @@ -275,9 +265,7 @@ export function validateDefinition( return validation } - if ( - !interfaceDefinitionExistsInFile(normalizedFilePath, modelName, language) - ) { + if (!findTypeInFile(normalizedFilePath, modelName)) { validation.interfaceExists = false } From 1d38810d39f2147b636ff8c266bd69cf0c1771e7 Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Sun, 4 Nov 2018 18:56:22 +0100 Subject: [PATCH 03/20] Enhanced introspection to easily resolve referenced type --- .../graphqlgen/src/introspection/ts-ast.ts | 163 +++++++++++------- packages/graphqlgen/src/types.ts | 4 +- 2 files changed, 105 insertions(+), 62 deletions(-) diff --git a/packages/graphqlgen/src/introspection/ts-ast.ts b/packages/graphqlgen/src/introspection/ts-ast.ts index 7aafd51a..3e94a7dc 100644 --- a/packages/graphqlgen/src/introspection/ts-ast.ts +++ b/packages/graphqlgen/src/introspection/ts-ast.ts @@ -28,31 +28,36 @@ import { BaseNode, isTSParenthesizedType, } from '@babel/types' -import chalk from "chalk"; +import chalk from 'chalk' -type Type = - | TypeAnnotation +type InnerType = + | ScalarTypeAnnotation | UnionTypeAnnotation | AnonymousInterfaceAnnotation | LiteralTypeAnnotation +type InternalInnerType = InnerType | TypeReferenceAnnotation + type UnknownType = '_UNKNOWN_TYPE_' -type PrimitiveOrTypeRef = string | null | UnknownType +type Scalar = 'string' | 'number' | 'boolean' | null | UnknownType + +export type TypeDefinition = InterfaceDefinition | TypeAliasDefinition + +export type InnerAndTypeDefinition = InnerType | TypeDefinition // /!\ If you add a supported type of field, make sure you update isSupportedField() as well type SupportedFields = TSPropertySignature interface UnionTypeAnnotation { kind: 'UnionTypeAnnotation' - types: Type[] + getTypes: Defer isArray: boolean } -interface TypeAnnotation { - kind: 'TypeAnnotation' - type: PrimitiveOrTypeRef +interface ScalarTypeAnnotation { + kind: 'ScalarTypeAnnotation' + type: Scalar isArray: boolean - isTypeRef: boolean } export interface AnonymousInterfaceAnnotation { @@ -61,6 +66,11 @@ export interface AnonymousInterfaceAnnotation { isArray: boolean } +interface TypeReferenceAnnotation { + kind: 'TypeReferenceAnnotation' + referenceType: string +} + interface LiteralTypeAnnotation { kind: 'LiteralTypeAnnotation' type: string @@ -68,20 +78,18 @@ interface LiteralTypeAnnotation { isArray: boolean } -type Kind = - | 'InterfaceDefinition' - | 'FieldDefinition' - | 'TypeAliasDefinition' - | 'TypeAliasValueDefinition' +type TypeDefinitionKind = 'InterfaceDefinition' | 'TypeAliasDefinition' + +type Defer = () => T interface BaseTypeDefinition { - kind: Kind + kind: TypeDefinitionKind name: string } interface FieldDefinition { name: string - type: Type + getType: Defer optional: boolean } @@ -90,14 +98,12 @@ export interface InterfaceDefinition extends BaseTypeDefinition { } export interface TypeAliasDefinition extends BaseTypeDefinition { - type: Type + getType: Defer isEnum: boolean //If type is UnionType && `types` are scalar strings } -export type Types = InterfaceDefinition | TypeAliasDefinition - export interface TypesMap { - [typeName: string]: Types + [typeName: string]: TypeDefinition } export interface InterfaceNamesToFile { @@ -111,14 +117,31 @@ export interface ModelField { type ExtractableType = TSTypeAliasDeclaration | TSInterfaceDeclaration -function createTypeAlias(name: string, type: Type): TypeAliasDefinition { +const filesToTypesMap: { [filePath: string]: TypesMap } = {} + +function buildTypeGetter( + type: InternalInnerType, + filePath: string, +): () => InnerAndTypeDefinition { + if (type.kind === 'TypeReferenceAnnotation') { + return () => filesToTypesMap[filePath][type.referenceType] + } else { + return () => type + } +} + +function createTypeAlias( + name: string, + type: InternalInnerType, + filePath: string, +): TypeAliasDefinition { return { kind: 'TypeAliasDefinition', name, - type, + getType: buildTypeGetter(type, filePath), isEnum: type.kind === 'UnionTypeAnnotation' && - type.types.every(unionType => { + type.getTypes().every(unionType => { return ( unionType.kind === 'LiteralTypeAnnotation' && unionType.isArray === false && @@ -130,12 +153,13 @@ function createTypeAlias(name: string, type: Type): TypeAliasDefinition { function createInterfaceField( name: string, - type: Type, + type: InternalInnerType, + filePath: string, optional: boolean, ): FieldDefinition { return { name, - type, + getType: buildTypeGetter(type, filePath), optional, } } @@ -158,39 +182,42 @@ interface TypeAnnotationOpts { } function createTypeAnnotation( - type: PrimitiveOrTypeRef, + type: Scalar, options?: TypeAnnotationOpts, -): TypeAnnotation { +): ScalarTypeAnnotation { let opts: TypeAnnotationOpts = {} if (options === undefined) { opts = { isArray: false, isTypeRef: false, isAny: false } } else { opts = { isArray: options.isArray === undefined ? false : options.isArray, - isTypeRef: options.isTypeRef === undefined ? false : options.isTypeRef, isAny: options.isAny === undefined ? false : options.isAny, } } const isArray = opts.isArray === undefined ? false : opts.isArray - const isTypeRef = opts.isTypeRef === undefined ? false : opts.isTypeRef return { - kind: 'TypeAnnotation', + kind: 'ScalarTypeAnnotation', type, isArray, - isTypeRef, } } function createUnionTypeAnnotation( - types: Type[], - isArray: boolean = false, + types: InternalInnerType[], + filePath: string, ): UnionTypeAnnotation { return { kind: 'UnionTypeAnnotation', - types, - isArray, + getTypes: () => { + return types.map(unionType => { + return unionType.kind === 'TypeReferenceAnnotation' + ? filesToTypesMap[filePath][unionType.referenceType] + : unionType + }) + }, + isArray: false, } } @@ -218,7 +245,13 @@ function createLiteralTypeAnnotation( } } -function computeType(node: TSType, filePath: string): Type { +function createTypeReferenceAnnotation( + referenceType: string, +): TypeReferenceAnnotation { + return { kind: 'TypeReferenceAnnotation', referenceType } +} + +function computeType(node: TSType, filePath: string): InternalInnerType { if (isTSParenthesizedType(node)) { node = node.typeAnnotation } @@ -235,21 +268,20 @@ function computeType(node: TSType, filePath: string): Type { if (isTSAnyKeyword(node)) { return createTypeAnnotation(null, { isAny: true }) } - if (isTSNullKeyword(node)) { - return createTypeAnnotation(null) - } if (isTSNullKeyword(node) || isTSUndefinedKeyword(node)) { return createTypeAnnotation(null) } if (isTSTypeReference(node)) { - const typeRefName = (node.typeName as Identifier).name + const referenceTypeName = (node.typeName as Identifier).name - return createTypeAnnotation(typeRefName, { isTypeRef: true }) + return createTypeReferenceAnnotation(referenceTypeName) } if (isTSArrayType(node)) { const computedType = computeType(node.elementType, filePath) - computedType.isArray = true + if (computedType.kind !== 'TypeReferenceAnnotation') { + computedType.isArray = true + } return computedType } @@ -267,15 +299,19 @@ function computeType(node: TSType, filePath: string): Type { } if (isTSUnionType(node)) { - const unionTypes = node.types.map(unionType => computeType(unionType, filePath)) + const unionTypes = node.types.map(unionType => + computeType(unionType, filePath), + ) - return createUnionTypeAnnotation(unionTypes) + return createUnionTypeAnnotation(unionTypes, filePath) } console.log( - chalk.yellow(`WARNING: Unsupported type ${node.type} (Line ${getLine( - node, - )} in ${filePath}). Please file an issue at https://github.com/prisma/graphqlgen/issues`), + chalk.yellow( + `WARNING: Unsupported type ${node.type} (Line ${getLine( + node, + )} in ${filePath}). Please file an issue at https://github.com/prisma/graphqlgen/issues`, + ), ) return createTypeAnnotation('_UNKNOWN_TYPE_') @@ -288,14 +324,18 @@ function getLine(node: BaseNode) { function extractTypeAlias( typeName: string, typeAlias: TSTypeAliasDeclaration, - filePath: string + filePath: string, ): TypeAliasDefinition | InterfaceDefinition { if (isTSTypeLiteral(typeAlias.typeAnnotation)) { - return extractInterface(typeName, typeAlias.typeAnnotation.members, filePath) + return extractInterface( + typeName, + typeAlias.typeAnnotation.members, + filePath, + ) } else { const typeAliasType = computeType(typeAlias.typeAnnotation, filePath) - return createTypeAlias(typeName, typeAliasType) + return createTypeAlias(typeName, typeAliasType, filePath) } } @@ -324,31 +364,34 @@ function extractInterfaceFields(fields: TSTypeElement[], filePath: string) { const fieldName = (field.key as Identifier).name if (!field.typeAnnotation) { - throw new Error(`ERROR: Unsupported notation (Line ${getLine(field)} in ${filePath})`) + throw new Error( + `ERROR: Unsupported notation (Line ${getLine(field)} in ${filePath})`, + ) } - const fieldType = computeType(field.typeAnnotation!.typeAnnotation, filePath) + const fieldType = computeType( + field.typeAnnotation!.typeAnnotation, + filePath, + ) const isOptional = isFieldOptional(field) - return createInterfaceField(fieldName, fieldType, isOptional) + return createInterfaceField(fieldName, fieldType, filePath, isOptional) }) } function extractInterface( typeName: string, fields: TSTypeElement[], - filePath: string + filePath: string, ): InterfaceDefinition { const interfaceFields = extractInterfaceFields(fields, filePath) return createInterface(typeName, interfaceFields) } -const cachedFilesToTypeMaps: { [filePath: string]: TypesMap } = {} - export function buildTypesMap(filePath: string): TypesMap { - if (cachedFilesToTypeMaps[filePath] !== undefined) { - return cachedFilesToTypeMaps[filePath] + if (filesToTypesMap[filePath] !== undefined) { + return filesToTypesMap[filePath] } const file = fs.readFileSync(filePath).toString() @@ -377,7 +420,7 @@ export function buildTypesMap(filePath: string): TypesMap { {} as TypesMap, ) - cachedFilesToTypeMaps[filePath] = typesMap + filesToTypesMap[filePath] = typesMap return typesMap } @@ -411,7 +454,7 @@ function findTypescriptTypes(sourceFile: TSFile): ExtractableType[] { export function findTypeInFile( filePath: string, typeName: string, -): Types | undefined { +): TypeDefinition | undefined { const filesToTypesMap = buildFilesToTypesMap([{ path: filePath }]) return filesToTypesMap[filePath][typeName] diff --git a/packages/graphqlgen/src/types.ts b/packages/graphqlgen/src/types.ts index 21133c52..bdfeffe0 100644 --- a/packages/graphqlgen/src/types.ts +++ b/packages/graphqlgen/src/types.ts @@ -4,7 +4,7 @@ import { GraphQLEnumObject, GraphQLUnionObject, } from './source-helper' -import { Types } from './introspection/ts-ast' +import { TypeDefinition } from './introspection/ts-ast' export interface GenerateArgs { types: GraphQLTypeObject[] @@ -21,7 +21,7 @@ export interface ModelMap { export interface Model { absoluteFilePath: string importPathRelativeToOutput: string - definition: Types + definition: TypeDefinition } export interface ContextDefinition { From eeb4859e01be76a1da5d93fc6b8807ae0d58c8cd Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Sun, 4 Nov 2018 18:59:20 +0100 Subject: [PATCH 04/20] Render inlined enums (from GraphQL schema) --- packages/graphqlgen/src/generators/ts-generator.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/graphqlgen/src/generators/ts-generator.ts b/packages/graphqlgen/src/generators/ts-generator.ts index ea956e99..7021d9e8 100644 --- a/packages/graphqlgen/src/generators/ts-generator.ts +++ b/packages/graphqlgen/src/generators/ts-generator.ts @@ -13,6 +13,7 @@ import { InputTypesMap, printFieldLikeType, getDistinctInputTypes, + renderEnums, } from './common' export function format(code: string, options: prettier.Options = {}) { @@ -66,6 +67,8 @@ export function generate(args: GenerateArgs): string { return `\ ${renderHeader(args)} + + ${renderEnums(args)} ${renderNamespaces(args, typeToInputTypeAssociation, inputTypesMap)} From 5ee86d485a2990f349a2b589039dc9d4d06a277a Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Sun, 4 Nov 2018 21:05:04 +0100 Subject: [PATCH 05/20] Implemented smart scaffolding for enums --- packages/graphqlgen/src/generators/common.ts | 101 +++++++++------- .../src/generators/flow-generator.ts | 16 ++- .../src/generators/flow-scaffolder.ts | 18 +-- .../graphqlgen/src/generators/ts-generator.ts | 16 ++- .../src/generators/ts-scaffolder.ts | 15 ++- .../graphqlgen/src/introspection/ts-ast.ts | 112 ++++++++++++++---- packages/graphqlgen/src/source-helper.ts | 19 +++ .../__snapshots__/basic.test.ts.snap | 2 + .../__snapshots__/large-schema.test.ts.snap | 18 +++ 9 files changed, 223 insertions(+), 94 deletions(-) diff --git a/packages/graphqlgen/src/generators/common.ts b/packages/graphqlgen/src/generators/common.ts index b5ad1e41..af9388c9 100644 --- a/packages/graphqlgen/src/generators/common.ts +++ b/packages/graphqlgen/src/generators/common.ts @@ -4,14 +4,17 @@ import { GraphQLTypeObject, GraphQLType, GraphQLTypeField, + getGraphQLEnumValues, } from '../source-helper' import { ModelMap, ContextDefinition, GenerateArgs } from '../types' import { - ModelField, - Types, + TypeDefinition, InterfaceDefinition, TypeAliasDefinition, AnonymousInterfaceAnnotation, + FieldDefinition, + isFieldDefinitionEnumOrLiteral, + getEnumValues, } from '../introspection/ts-ast' import { flatten, uniq } from '../utils' @@ -25,33 +28,24 @@ export interface TypeToInputTypeAssociation { [objectTypeName: string]: string[] } -export function fieldsFromModelDefinition(modelDef: Types): ModelField[] { +export function fieldsFromModelDefinition( + modelDef: TypeDefinition, +): FieldDefinition[] { // If model is of type `interface InterfaceName { ... }` if (modelDef.kind === 'InterfaceDefinition') { const interfaceDef = modelDef as InterfaceDefinition - return interfaceDef.fields.map(field => { - return { - fieldName: field.name, - fieldOptional: field.optional, - } - }) + return interfaceDef.fields } // If model is of type `type TypeName = { ... }` if ( modelDef.kind === 'TypeAliasDefinition' && - (modelDef as TypeAliasDefinition).type.kind === + (modelDef as TypeAliasDefinition).getType().kind === 'AnonymousInterfaceAnnotation' ) { - const interfaceDef = (modelDef as TypeAliasDefinition) - .type as AnonymousInterfaceAnnotation - - return interfaceDef.fields.map(field => { - return { - fieldName: field.name, - fieldOptional: field.optional, - } - }) + const interfaceDef = (modelDef as TypeAliasDefinition).getType() as AnonymousInterfaceAnnotation + + return interfaceDef.fields } return [] @@ -59,10 +53,10 @@ export function fieldsFromModelDefinition(modelDef: Types): ModelField[] { export function renderDefaultResolvers( graphQLTypeObject: GraphQLTypeObject, - modelMap: ModelMap, + args: GenerateArgs, variableName: string, ): string { - const model = modelMap[graphQLTypeObject.name] + const model = args.modelMap[graphQLTypeObject.name] if (model === undefined) { return `export const ${variableName} = {}` @@ -72,13 +66,17 @@ export function renderDefaultResolvers( return `export const ${variableName} = { ${fieldsFromModelDefinition(modelDef) - .filter(modelField => - shouldRenderDefaultResolver(graphQLTypeObject, modelField), - ) + .filter(modelField => { + const graphQLField = graphQLTypeObject.fields.find( + field => field.name === modelField.name, + ) + + return shouldRenderDefaultResolver(graphQLField, modelField, args) + }) .map(modelField => renderDefaultResolver( - modelField.fieldName, - modelField.fieldOptional, + modelField.name, + modelField.optional, model.definition.name, ), ) @@ -132,34 +130,57 @@ export function getModelName(type: GraphQLType, modelMap: ModelMap): string { return model.definition.name } -function shouldRenderDefaultResolver( - graphQLType: GraphQLTypeObject, - modelField: ModelField, +function isModelEnumSubsetOfGraphQLEnum( + graphQLEnumValues: string[], + modelEnumValues: string[], ) { - const graphQLField = graphQLType.fields.find( - field => field.name === modelField.fieldName, + return modelEnumValues.every(enumValue => + graphQLEnumValues.includes(enumValue), ) +} + +function shouldRenderDefaultResolver( + graphQLField: GraphQLTypeField | undefined, + modelField: FieldDefinition | undefined, + args: GenerateArgs, +) { + if (graphQLField === undefined) { + return false + } - if (!graphQLField) { + if (modelField == undefined) { return false } - return !(modelField.fieldOptional && graphQLField.type.isRequired) + const modelFieldType = modelField.getType() + + // If both types are enums, and model definition enum is a subset of the graphql enum + // Then render as defaultResolver + // eg: given GraphQLEnum = 'A' | 'B' | 'C' + // render when FieldDefinition = ('A') | ('A' | 'B') | ('A | 'B' | 'C') + if ( + graphQLField.type.isEnum && + isFieldDefinitionEnumOrLiteral(modelFieldType) + ) { + return isModelEnumSubsetOfGraphQLEnum( + getGraphQLEnumValues(graphQLField, args.enums), + getEnumValues(modelFieldType), + ) + } + + return !(modelField.optional && graphQLField.type.isRequired) } export function shouldScaffoldFieldResolver( graphQLField: GraphQLTypeField, - modelFields: ModelField[], + modelFields: FieldDefinition[], + args: GenerateArgs, ): boolean { const modelField = modelFields.find( - modelField => modelField.fieldName === graphQLField.name, + modelField => modelField.name === graphQLField.name, ) - if (!modelField) { - return true - } - - return modelField.fieldOptional && graphQLField.type.isRequired + return !shouldRenderDefaultResolver(graphQLField, modelField, args) } export function printFieldLikeType( diff --git a/packages/graphqlgen/src/generators/flow-generator.ts b/packages/graphqlgen/src/generators/flow-generator.ts index 2f43bd12..c2e61af6 100644 --- a/packages/graphqlgen/src/generators/flow-generator.ts +++ b/packages/graphqlgen/src/generators/flow-generator.ts @@ -116,8 +116,7 @@ function renderNamespaces( type, typeToInputTypeAssociation, inputTypesMap, - args.modelMap, - args.context, + args ), ) .join(os.EOL) @@ -127,27 +126,26 @@ function renderNamespace( type: GraphQLTypeObject, typeToInputTypeAssociation: TypeToInputTypeAssociation, inputTypesMap: InputTypesMap, - modelMap: ModelMap, - context?: ContextDefinition, + args: GenerateArgs ): string { const typeName = upperFirst(type.name) return `\ // Types for ${typeName} - ${renderDefaultResolvers(type, modelMap, `${typeName}_defaultResolvers`)} + ${renderDefaultResolvers(type, args, `${typeName}_defaultResolvers`)} ${renderInputTypeInterfaces( type, - modelMap, + args.modelMap, typeToInputTypeAssociation, inputTypesMap, )} - ${renderInputArgInterfaces(type, modelMap)} + ${renderInputArgInterfaces(type, args.modelMap)} - ${renderResolverFunctionInterfaces(type, modelMap, context)} + ${renderResolverFunctionInterfaces(type, args.modelMap, args.context)} - ${renderResolverTypeInterface(type, modelMap, context)} + ${renderResolverTypeInterface(type, args.modelMap, args.context)} ${/* TODO renderResolverClass(type, modelMap) */ ''} ` diff --git a/packages/graphqlgen/src/generators/flow-scaffolder.ts b/packages/graphqlgen/src/generators/flow-scaffolder.ts index f6931db8..e0115002 100644 --- a/packages/graphqlgen/src/generators/flow-scaffolder.ts +++ b/packages/graphqlgen/src/generators/flow-scaffolder.ts @@ -1,8 +1,10 @@ -import { GenerateArgs, CodeFileLike, ModelMap } from '../types' +import { GenerateArgs, CodeFileLike } from '../types' import { upperFirst } from '../utils' import { GraphQLTypeObject } from '../source-helper' -import { extractFieldsFromFlowType } from '../introspection/flow-ast' -import { shouldScaffoldFieldResolver } from './common' +import { + fieldsFromModelDefinition, + shouldScaffoldFieldResolver, +} from './common' export { format } from './flow-generator' @@ -33,10 +35,10 @@ function renderParentResolvers(type: GraphQLTypeObject): CodeFileLike { } function renderResolvers( type: GraphQLTypeObject, - modelMap: ModelMap, + args: GenerateArgs, ): CodeFileLike { - const model = modelMap[type.name] - const modelFields = extractFieldsFromFlowType(model) + const model = args.modelMap[type.name] + const modelFields = fieldsFromModelDefinition(model.definition) const upperTypeName = upperFirst(type.name) const code = `/* @flow */ import { ${upperTypeName}_defaultResolvers } from '[TEMPLATE-INTERFACES-PATH]' @@ -46,7 +48,7 @@ export const ${type.name}: ${upperTypeName}_Resolvers = { ...${upperTypeName}_defaultResolvers, ${type.fields .filter(graphQLField => - shouldScaffoldFieldResolver(graphQLField, modelFields), + shouldScaffoldFieldResolver(graphQLField, modelFields, args), ) .map( field => ` @@ -68,7 +70,7 @@ export function generate(args: GenerateArgs): CodeFileLike[] { let files: CodeFileLike[] = args.types .filter(type => type.type.isObject) .filter(type => !isParentType(type.name)) - .map(type => renderResolvers(type, args.modelMap)) + .map(type => renderResolvers(type, args)) files = files.concat( args.types diff --git a/packages/graphqlgen/src/generators/ts-generator.ts b/packages/graphqlgen/src/generators/ts-generator.ts index 7021d9e8..1e2ca5b9 100644 --- a/packages/graphqlgen/src/generators/ts-generator.ts +++ b/packages/graphqlgen/src/generators/ts-generator.ts @@ -128,8 +128,7 @@ function renderNamespaces( type, typeToInputTypeAssociation, inputTypesMap, - args.modelMap, - args.context, + args ), ) .join(os.EOL) @@ -139,26 +138,25 @@ function renderNamespace( graphQLTypeObject: GraphQLTypeObject, typeToInputTypeAssociation: TypeToInputTypeAssociation, inputTypesMap: InputTypesMap, - modelMap: ModelMap, - context?: ContextDefinition, + args: GenerateArgs ): string { return `\ export namespace ${graphQLTypeObject.name}Resolvers { - ${renderDefaultResolvers(graphQLTypeObject, modelMap, 'defaultResolvers')} + ${renderDefaultResolvers(graphQLTypeObject, args, 'defaultResolvers')} ${renderInputTypeInterfaces( graphQLTypeObject, - modelMap, + args.modelMap, typeToInputTypeAssociation, inputTypesMap, )} - ${renderInputArgInterfaces(graphQLTypeObject, modelMap)} + ${renderInputArgInterfaces(graphQLTypeObject, args.modelMap)} - ${renderResolverFunctionInterfaces(graphQLTypeObject, modelMap, context)} + ${renderResolverFunctionInterfaces(graphQLTypeObject, args.modelMap, args.context)} - ${renderResolverTypeInterface(graphQLTypeObject, modelMap, context)} + ${renderResolverTypeInterface(graphQLTypeObject, args.modelMap, args.context)} ${/* TODO renderResolverClass(type, modelMap) */ ''} } diff --git a/packages/graphqlgen/src/generators/ts-scaffolder.ts b/packages/graphqlgen/src/generators/ts-scaffolder.ts index 06c92614..880976ee 100644 --- a/packages/graphqlgen/src/generators/ts-scaffolder.ts +++ b/packages/graphqlgen/src/generators/ts-scaffolder.ts @@ -1,6 +1,9 @@ -import { GenerateArgs, CodeFileLike, ModelMap } from '../types' +import { GenerateArgs, CodeFileLike } from '../types' import { GraphQLTypeField, GraphQLTypeObject } from '../source-helper' -import { fieldsFromModelDefinition, shouldScaffoldFieldResolver } from "./common"; +import { + fieldsFromModelDefinition, + shouldScaffoldFieldResolver, +} from './common' export { format } from './ts-generator' @@ -44,9 +47,9 @@ function isParentType(name: string) { function renderResolvers( type: GraphQLTypeObject, - modelMap: ModelMap, + args: GenerateArgs, ): CodeFileLike { - const model = modelMap[type.name] + const model = args.modelMap[type.name] const modelFields = fieldsFromModelDefinition(model.definition) const code = `\ @@ -58,7 +61,7 @@ function renderResolvers( export const ${type.name}: ${type.name}Resolvers.Type = { ...${type.name}Resolvers.defaultResolvers, ${type.fields - .filter(field => shouldScaffoldFieldResolver(field, modelFields)) + .filter(field => shouldScaffoldFieldResolver(field, modelFields, args)) .map( field => ` ${field.name}: (parent${field.arguments.length > 0 ? ', args' : ''}) => { @@ -121,7 +124,7 @@ export function generate(args: GenerateArgs): CodeFileLike[] { let files: CodeFileLike[] = args.types .filter(type => type.type.isObject) .filter(type => !isParentType(type.name)) - .map(type => renderResolvers(type, args.modelMap)) + .map(type => renderResolvers(type, args)) files = files.concat( args.types diff --git a/packages/graphqlgen/src/introspection/ts-ast.ts b/packages/graphqlgen/src/introspection/ts-ast.ts index 3e94a7dc..c66b36aa 100644 --- a/packages/graphqlgen/src/introspection/ts-ast.ts +++ b/packages/graphqlgen/src/introspection/ts-ast.ts @@ -48,10 +48,11 @@ export type InnerAndTypeDefinition = InnerType | TypeDefinition // /!\ If you add a supported type of field, make sure you update isSupportedField() as well type SupportedFields = TSPropertySignature -interface UnionTypeAnnotation { +export interface UnionTypeAnnotation { kind: 'UnionTypeAnnotation' getTypes: Defer isArray: boolean + isEnum: Defer } interface ScalarTypeAnnotation { @@ -78,28 +79,27 @@ interface LiteralTypeAnnotation { isArray: boolean } -type TypeDefinitionKind = 'InterfaceDefinition' | 'TypeAliasDefinition' - type Defer = () => T interface BaseTypeDefinition { - kind: TypeDefinitionKind name: string } -interface FieldDefinition { +export interface FieldDefinition { name: string getType: Defer optional: boolean } export interface InterfaceDefinition extends BaseTypeDefinition { + kind: 'InterfaceDefinition' fields: FieldDefinition[] } export interface TypeAliasDefinition extends BaseTypeDefinition { + kind: 'TypeAliasDefinition' getType: Defer - isEnum: boolean //If type is UnionType && `types` are scalar strings + isEnum: Defer //If type is UnionType && `types` are scalar strings } export interface TypesMap { @@ -130,6 +130,77 @@ function buildTypeGetter( } } +export function isFieldDefinitionEnumOrLiteral( + modelFieldType: InnerAndTypeDefinition, +): boolean { + // If type is: 'value' + if (isLiteralString(modelFieldType)) { + return true + } + + if ( + modelFieldType.kind === 'UnionTypeAnnotation' && + modelFieldType.isEnum() + ) { + return true + } + + // If type is: type X = 'value' + if ( + modelFieldType.kind === 'TypeAliasDefinition' && + isLiteralString(modelFieldType.getType()) + ) { + return true + } + + // If type is: Type X = 'value' | 'value2' + return ( + modelFieldType.kind === 'TypeAliasDefinition' && modelFieldType.isEnum() + ) +} + +function isLiteralString(type: InnerAndTypeDefinition) { + return type.kind === 'LiteralTypeAnnotation' && type.type === 'string' +} + +export function getEnumValues(type: InnerAndTypeDefinition): string[] { + // If type is: 'value' + if (isLiteralString(type)) { + return [(type as LiteralTypeAnnotation).value as string] + } + + if ( + type.kind === 'TypeAliasDefinition' && + isLiteralString(type.getType()) + ) { + return [(type.getType() as LiteralTypeAnnotation).value as string] + } + + let unionTypes: InnerAndTypeDefinition[] = [] + + if (type.kind === 'TypeAliasDefinition' && type.isEnum()) { + unionTypes = (type.getType() as UnionTypeAnnotation).getTypes() + } else if (type.kind === 'UnionTypeAnnotation' && type.isEnum) { + unionTypes = type.getTypes() + } else { + return [] + } + + return unionTypes.map(unionType => { + return (unionType as LiteralTypeAnnotation).value + }) as string[] +} + +function isEnumUnion(unionTypes: InnerAndTypeDefinition[]) { + return unionTypes.every(unionType => { + return ( + unionType.kind === 'LiteralTypeAnnotation' && + unionType.isArray === false && + unionType.type === 'string' + ) + }) +} + function createTypeAlias( name: string, type: InternalInnerType, @@ -139,15 +210,9 @@ function createTypeAlias( kind: 'TypeAliasDefinition', name, getType: buildTypeGetter(type, filePath), - isEnum: - type.kind === 'UnionTypeAnnotation' && - type.getTypes().every(unionType => { - return ( - unionType.kind === 'LiteralTypeAnnotation' && - unionType.isArray === false && - unionType.type === 'string' - ) - }), + isEnum: () => { + return type.kind === 'UnionTypeAnnotation' && isEnumUnion(type.getTypes()) + }, } } @@ -208,16 +273,19 @@ function createUnionTypeAnnotation( types: InternalInnerType[], filePath: string, ): UnionTypeAnnotation { + const getTypes = () => { + return types.map(unionType => { + return unionType.kind === 'TypeReferenceAnnotation' + ? filesToTypesMap[filePath][unionType.referenceType] + : unionType + }) + } + return { kind: 'UnionTypeAnnotation', - getTypes: () => { - return types.map(unionType => { - return unionType.kind === 'TypeReferenceAnnotation' - ? filesToTypesMap[filePath][unionType.referenceType] - : unionType - }) - }, + getTypes, isArray: false, + isEnum: () => isEnumUnion(getTypes()), } } diff --git a/packages/graphqlgen/src/source-helper.ts b/packages/graphqlgen/src/source-helper.ts index f9374f06..6f004602 100644 --- a/packages/graphqlgen/src/source-helper.ts +++ b/packages/graphqlgen/src/source-helper.ts @@ -356,3 +356,22 @@ export function extractGraphQLTypesWithoutRootsAndInputsAndEnums( type => ['Query', 'Mutation', 'Subscription'].indexOf(type.name) === -1, ) } + +export function getGraphQLEnumValues( + enumField: GraphQLTypeField, + graphQLEnumObjects: GraphQLEnumObject[], +): string[] { + if (!enumField.type.isEnum) { + return [] + } + + const graphQLEnumObject = graphQLEnumObjects.find( + graphqlEnum => graphqlEnum.name === enumField.type.name, + ) + + if (!graphQLEnumObject) { + return [] + } + + return graphQLEnumObject.values +} diff --git a/packages/graphqlgen/src/tests/typescript/__snapshots__/basic.test.ts.snap b/packages/graphqlgen/src/tests/typescript/__snapshots__/basic.test.ts.snap index cf3253ab..493e069c 100644 --- a/packages/graphqlgen/src/tests/typescript/__snapshots__/basic.test.ts.snap +++ b/packages/graphqlgen/src/tests/typescript/__snapshots__/basic.test.ts.snap @@ -8,6 +8,8 @@ import { User } from \\"../../../fixtures/enum/types\\"; type Context = any; +type UserType = \\"ADMIN\\" | \\"EDITOR\\" | \\"COLLABORATOR\\"; + export namespace QueryResolvers { export const defaultResolvers = {}; diff --git a/packages/graphqlgen/src/tests/typescript/__snapshots__/large-schema.test.ts.snap b/packages/graphqlgen/src/tests/typescript/__snapshots__/large-schema.test.ts.snap index ae1091c9..86073853 100644 --- a/packages/graphqlgen/src/tests/typescript/__snapshots__/large-schema.test.ts.snap +++ b/packages/graphqlgen/src/tests/typescript/__snapshots__/large-schema.test.ts.snap @@ -35,6 +35,24 @@ import { MutationResult } from \\"../../../fixtures/prisma/types\\"; type Context = any; +type PLACE_SIZES = + | \\"ENTIRE_HOUSE\\" + | \\"ENTIRE_APARTMENT\\" + | \\"ENTIRE_EARTH_HOUSE\\" + | \\"ENTIRE_CABIN\\" + | \\"ENTIRE_VILLA\\" + | \\"ENTIRE_PLACE\\" + | \\"ENTIRE_BOAT\\" + | \\"PRIVATE_ROOM\\"; +type CURRENCY = \\"CAD\\" | \\"CHF\\" | \\"EUR\\" | \\"JPY\\" | \\"USD\\" | \\"ZAR\\"; +type PAYMENT_PROVIDER = \\"PAYPAL\\" | \\"CREDIT_CARD\\"; +type NOTIFICATION_TYPE = + | \\"OFFER\\" + | \\"INSTANT_BOOK\\" + | \\"RESPONSIVENESS\\" + | \\"NEW_AMENITIES\\" + | \\"HOUSE_RULES\\"; + export namespace QueryResolvers { export const defaultResolvers = {}; From 561ac1294de4e35767b32f7bdfe082bae257bfae Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Sun, 4 Nov 2018 23:34:00 +0100 Subject: [PATCH 06/20] Refactor typescript parser to make flow support smart enums --- packages/graphqlgen/src/generators/common.ts | 8 +- .../src/generators/flow-generator.ts | 18 +- .../graphqlgen/src/generators/ts-generator.ts | 25 +- .../graphqlgen/src/introspection/factory.ts | 126 ++++++ .../graphqlgen/src/introspection/flow-ast.ts | 232 ++++++++-- .../graphqlgen/src/introspection/index.ts | 58 +++ .../graphqlgen/src/introspection/ts-ast.ts | 413 +++--------------- .../graphqlgen/src/introspection/types.ts | 82 ++++ .../graphqlgen/src/introspection/utils.ts | 93 ++++ packages/graphqlgen/src/parse.ts | 20 +- .../flow/__snapshots__/basic.test.ts.snap | 2 + .../__snapshots__/large-schema.test.ts.snap | 18 + .../tests/introspection/typescript.test.ts | 7 +- packages/graphqlgen/src/types.ts | 2 +- packages/graphqlgen/src/utils.ts | 4 +- packages/graphqlgen/src/validation.ts | 12 +- 16 files changed, 683 insertions(+), 437 deletions(-) create mode 100644 packages/graphqlgen/src/introspection/factory.ts create mode 100644 packages/graphqlgen/src/introspection/index.ts create mode 100644 packages/graphqlgen/src/introspection/types.ts create mode 100644 packages/graphqlgen/src/introspection/utils.ts diff --git a/packages/graphqlgen/src/generators/common.ts b/packages/graphqlgen/src/generators/common.ts index af9388c9..c73d59e1 100644 --- a/packages/graphqlgen/src/generators/common.ts +++ b/packages/graphqlgen/src/generators/common.ts @@ -7,16 +7,18 @@ import { getGraphQLEnumValues, } from '../source-helper' import { ModelMap, ContextDefinition, GenerateArgs } from '../types' +import { flatten, uniq } from '../utils' import { TypeDefinition, + FieldDefinition, InterfaceDefinition, TypeAliasDefinition, AnonymousInterfaceAnnotation, - FieldDefinition, +} from '../introspection/types' +import { isFieldDefinitionEnumOrLiteral, getEnumValues, -} from '../introspection/ts-ast' -import { flatten, uniq } from '../utils' +} from '../introspection/utils' type SpecificGraphQLScalarType = 'boolean' | 'number' | 'string' diff --git a/packages/graphqlgen/src/generators/flow-generator.ts b/packages/graphqlgen/src/generators/flow-generator.ts index c2e61af6..346731ad 100644 --- a/packages/graphqlgen/src/generators/flow-generator.ts +++ b/packages/graphqlgen/src/generators/flow-generator.ts @@ -5,13 +5,14 @@ import { GenerateArgs, ModelMap, ContextDefinition } from '../types' import { GraphQLTypeField, GraphQLTypeObject } from '../source-helper' import { upperFirst } from '../utils' import { - renderDefaultResolvers, getContextName, + getDistinctInputTypes, getModelName, - TypeToInputTypeAssociation, InputTypesMap, printFieldLikeType, - getDistinctInputTypes, + renderDefaultResolvers, + renderEnums, + TypeToInputTypeAssociation, } from './common' export function format(code: string, options: prettier.Options = {}) { @@ -66,6 +67,8 @@ export function generate(args: GenerateArgs): string { return `\ ${renderHeader(args)} + ${renderEnums(args)} + ${renderNamespaces(args, typeToInputTypeAssociation, inputTypesMap)} ${renderResolvers(args)} @@ -112,12 +115,7 @@ function renderNamespaces( return args.types .filter(type => type.type.isObject) .map(type => - renderNamespace( - type, - typeToInputTypeAssociation, - inputTypesMap, - args - ), + renderNamespace(type, typeToInputTypeAssociation, inputTypesMap, args), ) .join(os.EOL) } @@ -126,7 +124,7 @@ function renderNamespace( type: GraphQLTypeObject, typeToInputTypeAssociation: TypeToInputTypeAssociation, inputTypesMap: InputTypesMap, - args: GenerateArgs + args: GenerateArgs, ): string { const typeName = upperFirst(type.name) diff --git a/packages/graphqlgen/src/generators/ts-generator.ts b/packages/graphqlgen/src/generators/ts-generator.ts index 1e2ca5b9..50d82fe3 100644 --- a/packages/graphqlgen/src/generators/ts-generator.ts +++ b/packages/graphqlgen/src/generators/ts-generator.ts @@ -3,8 +3,6 @@ import * as prettier from 'prettier' import { GenerateArgs, ModelMap, ContextDefinition } from '../types' import { GraphQLTypeField, GraphQLTypeObject } from '../source-helper' -import { TypeAliasDefinition } from '../introspection/ts-ast' -import { upperFirst } from '../utils' import { renderDefaultResolvers, getContextName, @@ -15,6 +13,8 @@ import { getDistinctInputTypes, renderEnums, } from './common' +import { TypeAliasDefinition } from '../introspection/types' +import { upperFirst } from '../utils' export function format(code: string, options: prettier.Options = {}) { try { @@ -124,12 +124,7 @@ function renderNamespaces( return args.types .filter(type => type.type.isObject) .map(type => - renderNamespace( - type, - typeToInputTypeAssociation, - inputTypesMap, - args - ), + renderNamespace(type, typeToInputTypeAssociation, inputTypesMap, args), ) .join(os.EOL) } @@ -138,7 +133,7 @@ function renderNamespace( graphQLTypeObject: GraphQLTypeObject, typeToInputTypeAssociation: TypeToInputTypeAssociation, inputTypesMap: InputTypesMap, - args: GenerateArgs + args: GenerateArgs, ): string { return `\ export namespace ${graphQLTypeObject.name}Resolvers { @@ -154,9 +149,17 @@ function renderNamespace( ${renderInputArgInterfaces(graphQLTypeObject, args.modelMap)} - ${renderResolverFunctionInterfaces(graphQLTypeObject, args.modelMap, args.context)} + ${renderResolverFunctionInterfaces( + graphQLTypeObject, + args.modelMap, + args.context, + )} - ${renderResolverTypeInterface(graphQLTypeObject, args.modelMap, args.context)} + ${renderResolverTypeInterface( + graphQLTypeObject, + args.modelMap, + args.context, + )} ${/* TODO renderResolverClass(type, modelMap) */ ''} } diff --git a/packages/graphqlgen/src/introspection/factory.ts b/packages/graphqlgen/src/introspection/factory.ts new file mode 100644 index 00000000..ef8e60b9 --- /dev/null +++ b/packages/graphqlgen/src/introspection/factory.ts @@ -0,0 +1,126 @@ +import { + InternalInnerType, + TypeAliasDefinition, + FieldDefinition, + InterfaceDefinition, + Scalar, + ScalarTypeAnnotation, + UnionTypeAnnotation, + AnonymousInterfaceAnnotation, + LiteralTypeAnnotation, + TypeReferenceAnnotation, + UnknownType, +} from './types' +import { buildTypeGetter, isEnumUnion } from './utils' +import { filesToTypesMap } from './index' + +export function createTypeAlias( + name: string, + type: InternalInnerType, + filePath: string, +): TypeAliasDefinition { + return { + kind: 'TypeAliasDefinition', + name, + getType: buildTypeGetter(type, filePath), + isEnum: () => { + return type.kind === 'UnionTypeAnnotation' && isEnumUnion(type.getTypes()) + }, + } +} +export function createInterfaceField( + name: string, + type: InternalInnerType, + filePath: string, + optional: boolean, +): FieldDefinition { + return { + name, + getType: buildTypeGetter(type, filePath), + optional, + } +} +export function createInterface( + name: string, + fields: FieldDefinition[], +): InterfaceDefinition { + return { + kind: 'InterfaceDefinition', + name, + fields, + } +} + +interface TypeAnnotationOpts { + isArray?: boolean + isTypeRef?: boolean + isAny?: boolean +} +export function createTypeAnnotation( + type: Scalar | UnknownType, + options?: TypeAnnotationOpts, +): ScalarTypeAnnotation { + let opts: TypeAnnotationOpts = {} + if (options === undefined) { + opts = { isArray: false, isTypeRef: false, isAny: false } + } else { + opts = { + isArray: options.isArray === undefined ? false : options.isArray, + isAny: options.isAny === undefined ? false : options.isAny, + } + } + + const isArray = opts.isArray === undefined ? false : opts.isArray + + return { + kind: 'ScalarTypeAnnotation', + type, + isArray, + } +} +export function createUnionTypeAnnotation( + types: InternalInnerType[], + filePath: string, +): UnionTypeAnnotation { + const getTypes = () => { + return types.map(unionType => { + return unionType.kind === 'TypeReferenceAnnotation' + ? filesToTypesMap[filePath][unionType.referenceType] + : unionType + }) + } + + return { + kind: 'UnionTypeAnnotation', + getTypes, + isArray: false, + isEnum: () => isEnumUnion(getTypes()), + } +} +export function createAnonymousInterfaceAnnotation( + fields: FieldDefinition[], + isArray: boolean = false, +): AnonymousInterfaceAnnotation { + return { + kind: 'AnonymousInterfaceAnnotation', + fields, + isArray, + } +} +export function createLiteralTypeAnnotation( + type: string, + value: string | number | boolean, + isArray: boolean = false, +): LiteralTypeAnnotation { + return { + kind: 'LiteralTypeAnnotation', + type, + value, + isArray, + } +} +export function createTypeReferenceAnnotation( + referenceType: string, +): TypeReferenceAnnotation { + return { kind: 'TypeReferenceAnnotation', referenceType } +} diff --git a/packages/graphqlgen/src/introspection/flow-ast.ts b/packages/graphqlgen/src/introspection/flow-ast.ts index 70e9affc..6fd9f07f 100644 --- a/packages/graphqlgen/src/introspection/flow-ast.ts +++ b/packages/graphqlgen/src/introspection/flow-ast.ts @@ -7,18 +7,51 @@ import { Statement, TypeAlias, InterfaceDeclaration, - ObjectTypeAnnotation, ObjectTypeProperty, - Identifier, UnionTypeAnnotation, + isTypeAlias, + ObjectTypeSpreadProperty, + isObjectTypeProperty, + Identifier, + StringLiteral, + isObjectTypeAnnotation, + FlowType, + isStringTypeAnnotation, + isNumberTypeAnnotation, + isBooleanTypeAnnotation, + isAnyTypeAnnotation, + isGenericTypeAnnotation, + isArrayTypeAnnotation, + isStringLiteralTypeAnnotation, + isNumberLiteralTypeAnnotation, + isBooleanLiteralTypeAnnotation, + isUnionTypeAnnotation, + isNullLiteralTypeAnnotation, } from '@babel/types' import { File } from 'graphqlgen-json-schema' import { getPath } from '../parse' -import { Model } from '../types' -import { ModelField } from './ts-ast' +import { + TypeAliasDefinition, + InterfaceDefinition, + TypesMap, + InternalInnerType, +} from './types' +import { + createInterface, + createInterfaceField, + createTypeAlias, + createTypeAnnotation, + createTypeReferenceAnnotation, + createLiteralTypeAnnotation, + createAnonymousInterfaceAnnotation, + createUnionTypeAnnotation, +} from './factory' +import { getLine } from './utils' +import chalk from 'chalk' -//TODO: Add caching with { [filePath: string]: ExtractableType[] } or something +// /!\ If you add a supported type of field, make sure you update isSupportedField() as well +type SupportedFields = ObjectTypeProperty type ExtractableType = TypeAlias | InterfaceDeclaration function getSourceFile(filePath: string): FlowFile { @@ -31,7 +64,7 @@ function shouldExtractType(node: Statement) { return node.type === 'TypeAlias' || node.type === 'InterfaceDeclaration' } -function getFlowTypes(sourceFile: FlowFile): ExtractableType[] { +function findFlowTypes(sourceFile: FlowFile): ExtractableType[] { const statements = sourceFile.program.body const types = statements.filter(shouldExtractType) @@ -50,23 +83,14 @@ function getFlowTypes(sourceFile: FlowFile): ExtractableType[] { return [...types, ...typesFromNamedExport] as ExtractableType[] } -export function findFlowTypeByName( - filePath: string, - typeName: string, -): ExtractableType | undefined { - const sourceFile = getSourceFile(filePath) - - return getFlowTypes(sourceFile).find(node => node.id.name === typeName) -} - export function typeNamesFromFlowFile(file: File): string[] { const filePath = getPath(file) const sourceFile = getSourceFile(filePath) - return getFlowTypes(sourceFile).map(node => node.id.name) + return findFlowTypes(sourceFile).map(node => node.id.name) } -function isFieldOptional(node: ObjectTypeProperty) { +export function isFieldOptional(node: ObjectTypeProperty) { if (!!node.optional) { return true } @@ -84,26 +108,166 @@ function isFieldOptional(node: ObjectTypeProperty) { return false } -export function extractFieldsFromFlowType(model: Model): ModelField[] { - const filePath = model.absoluteFilePath - const typeNode = findFlowTypeByName(filePath, model.definition.name) +function isSupportedTypeOfField( + field: ObjectTypeProperty | ObjectTypeSpreadProperty, +) { + return isObjectTypeProperty(field) +} + +function throwIfUnsupportedFields( + fields: (ObjectTypeProperty | ObjectTypeSpreadProperty)[], + filePath: string, +) { + const unsupportedFields = fields.filter( + field => !isSupportedTypeOfField(field), + ) + + if (unsupportedFields.length > 0) { + throw new Error( + `Unsupported notation for fields: ${unsupportedFields + .map(field => `Line ${getLine(field)} in ${filePath}`) + .join(', ')}`, + ) + } +} + +export function computeType( + node: FlowType, + filePath: string, +): InternalInnerType { + if (isStringTypeAnnotation(node)) { + return createTypeAnnotation('string') + } + if (isNumberTypeAnnotation(node)) { + return createTypeAnnotation('number') + } + if (isBooleanTypeAnnotation(node)) { + return createTypeAnnotation('boolean') + } + if (isAnyTypeAnnotation(node)) { + return createTypeAnnotation(null, { isAny: true }) + } + if ( + (isGenericTypeAnnotation(node) && node.id.name === 'undefined') || + isNullLiteralTypeAnnotation(node) + ) { + return createTypeAnnotation(null) + } + if (isGenericTypeAnnotation(node)) { + const referenceTypeName = node.id.name - if (!typeNode) { - throw new Error(`No interface found for name ${model.definition}`) + return createTypeReferenceAnnotation(referenceTypeName) } + if (isArrayTypeAnnotation(node)) { + const computedType = computeType(node.elementType, filePath) - const childrenNodes = - typeNode.type === 'TypeAlias' - ? (typeNode as TypeAlias).right - : (typeNode as InterfaceDeclaration).body + if (computedType.kind !== 'TypeReferenceAnnotation') { + computedType.isArray = true + } - return (childrenNodes as ObjectTypeAnnotation).properties - .filter(childNode => childNode.type === 'ObjectTypeProperty') - .map(childNode => { - const childNodeProperty = childNode as ObjectTypeProperty - const fieldName = (childNodeProperty.key as Identifier).name - const fieldOptional = isFieldOptional(childNodeProperty) + return computedType + } + if ( + isStringLiteralTypeAnnotation(node) || + isNumberLiteralTypeAnnotation(node) || + isBooleanLiteralTypeAnnotation(node) + ) { + const literalValue = node.value - return { fieldName, fieldOptional } - }) + return createLiteralTypeAnnotation(typeof literalValue, literalValue) + } + + if (isObjectTypeAnnotation(node)) { + const interfaceFields = extractInterfaceFields(node.properties, filePath) + + return createAnonymousInterfaceAnnotation(interfaceFields) + } + + if (isUnionTypeAnnotation(node)) { + const unionTypes = node.types.map(unionType => + computeType(unionType, filePath), + ) + + return createUnionTypeAnnotation(unionTypes, filePath) + } + + console.log( + chalk.yellow( + `WARNING: Unsupported type ${node.type} (Line ${getLine( + node, + )} in ${filePath}). Please file an issue at https://github.com/prisma/graphqlgen/issues`, + ), + ) + + return createTypeAnnotation('_UNKNOWN_TYPE_') +} + +function extractTypeAlias( + typeName: string, + typeAlias: TypeAlias, + filePath: string, +): TypeAliasDefinition | InterfaceDefinition { + if (isObjectTypeAnnotation(typeAlias.right)) { + return extractInterface(typeName, typeAlias.right.properties, filePath) + } else { + const typeAliasType = computeType(typeAlias.right, filePath) + + return createTypeAlias(typeName, typeAliasType, filePath) + } +} + +function extractInterfaceFields( + fields: (ObjectTypeProperty | ObjectTypeSpreadProperty)[], + filePath: string, +) { + throwIfUnsupportedFields(fields, filePath) + + return (fields as SupportedFields[]).map(field => { + const fieldName = + field.key.type === 'Identifier' + ? (field.key as Identifier).name + : (field.key as StringLiteral).value + + const fieldType = computeType(field.value, filePath) + const isOptional = isFieldOptional(field) + + return createInterfaceField(fieldName, fieldType, filePath, isOptional) + }) +} + +function extractInterface( + typeName: string, + fields: (ObjectTypeProperty | ObjectTypeSpreadProperty)[], + filePath: string, +): InterfaceDefinition { + const interfaceFields = extractInterfaceFields(fields, filePath) + + return createInterface(typeName, interfaceFields) +} + +export function buildFlowTypesMap(fileContent: string, filePath: string) { + const ast = parseFlow(fileContent, { + plugins: ['flow'], + }) + + const typesMap = findFlowTypes(ast).reduce( + (acc, type) => { + const typeName = type.id.name + + if (isTypeAlias(type)) { + return { + ...acc, + [typeName]: extractTypeAlias(typeName, type, filePath), + } + } + + return { + ...acc, + [typeName]: extractInterface(typeName, type.body.properties, filePath), + } + }, + {} as TypesMap, + ) + + return typesMap } diff --git a/packages/graphqlgen/src/introspection/index.ts b/packages/graphqlgen/src/introspection/index.ts new file mode 100644 index 00000000..e466ddbb --- /dev/null +++ b/packages/graphqlgen/src/introspection/index.ts @@ -0,0 +1,58 @@ +import * as fs from 'fs' +import { Language } from 'graphqlgen-json-schema' + +import { FilesToTypesMap, TypeDefinition, TypesMap } from './types' +import { buildTSTypesMap } from './ts-ast' +import { buildFlowTypesMap } from './flow-ast' +import { NormalizedFile } from '../parse' + +export const filesToTypesMap: { [filePath: string]: TypesMap } = {} + +function buildTypesMapByLanguage( + fileContent: string, + filePath: string, + language: Language, +): TypesMap { + switch (language) { + case 'typescript': + return buildTSTypesMap(fileContent, filePath) + case 'flow': + return buildFlowTypesMap(fileContent, filePath) + } +} + +export function buildTypesMap(filePath: string, language: Language): TypesMap { + if (filesToTypesMap[filePath] !== undefined) { + return filesToTypesMap[filePath] + } + + const fileContent = fs.readFileSync(filePath).toString() + + const typesMap = buildTypesMapByLanguage(fileContent, filePath, language) + + filesToTypesMap[filePath] = typesMap + + return typesMap +} + +export function findTypeInFile( + filePath: string, + typeName: string, + language: Language, +): TypeDefinition | undefined { + const filesToTypesMap = buildFilesToTypesMap([{ path: filePath }], language) + + return filesToTypesMap[filePath][typeName] +} + +export function buildFilesToTypesMap( + files: NormalizedFile[], + language: Language, +): FilesToTypesMap { + return files.reduce((acc, file) => { + return { + ...acc, + [file.path]: buildTypesMap(file.path, language), + } + }, {}) +} diff --git a/packages/graphqlgen/src/introspection/ts-ast.ts b/packages/graphqlgen/src/introspection/ts-ast.ts index c66b36aa..167a16a5 100644 --- a/packages/graphqlgen/src/introspection/ts-ast.ts +++ b/packages/graphqlgen/src/introspection/ts-ast.ts @@ -1,325 +1,62 @@ -import * as fs from 'fs' -import { File } from 'graphqlgen-json-schema' -import { buildFilesToTypesMap } from '../parse' import { parse as parseTS } from '@babel/parser' import { ExportNamedDeclaration, File as TSFile, Identifier, - isTSArrayType, - isTSBooleanKeyword, - isTSLiteralType, - isTSNumberKeyword, isTSPropertySignature, - isTSStringKeyword, - isTSTypeReference, - isTSUnionType, + isTSTypeLiteral, Statement, TSInterfaceDeclaration, TSPropertySignature, - TSType, TSTypeAliasDeclaration, + TSTypeElement, TSUnionType, + TSType, + isTSParenthesizedType, + isTSStringKeyword, + isTSNumberKeyword, + isTSBooleanKeyword, isTSAnyKeyword, isTSNullKeyword, isTSUndefinedKeyword, - isTSTypeLiteral, - TSTypeElement, - BaseNode, - isTSParenthesizedType, + isTSTypeReference, + isTSArrayType, + isTSLiteralType, + isTSUnionType, + isTSTypeAliasDeclaration, } from '@babel/types' +import { + InterfaceDefinition, + TypeAliasDefinition, + TypesMap, + InternalInnerType, +} from './types' +import { + createInterface, + createInterfaceField, + createTypeAlias, + createTypeAnnotation, + createTypeReferenceAnnotation, + createLiteralTypeAnnotation, + createAnonymousInterfaceAnnotation, + createUnionTypeAnnotation, +} from './factory' +import { getLine } from './utils' import chalk from 'chalk' -type InnerType = - | ScalarTypeAnnotation - | UnionTypeAnnotation - | AnonymousInterfaceAnnotation - | LiteralTypeAnnotation - -type InternalInnerType = InnerType | TypeReferenceAnnotation - -type UnknownType = '_UNKNOWN_TYPE_' -type Scalar = 'string' | 'number' | 'boolean' | null | UnknownType - -export type TypeDefinition = InterfaceDefinition | TypeAliasDefinition - -export type InnerAndTypeDefinition = InnerType | TypeDefinition - // /!\ If you add a supported type of field, make sure you update isSupportedField() as well type SupportedFields = TSPropertySignature -export interface UnionTypeAnnotation { - kind: 'UnionTypeAnnotation' - getTypes: Defer - isArray: boolean - isEnum: Defer -} - -interface ScalarTypeAnnotation { - kind: 'ScalarTypeAnnotation' - type: Scalar - isArray: boolean -} - -export interface AnonymousInterfaceAnnotation { - kind: 'AnonymousInterfaceAnnotation' - fields: FieldDefinition[] - isArray: boolean -} - -interface TypeReferenceAnnotation { - kind: 'TypeReferenceAnnotation' - referenceType: string -} - -interface LiteralTypeAnnotation { - kind: 'LiteralTypeAnnotation' - type: string - value: string | number | boolean - isArray: boolean -} - -type Defer = () => T - -interface BaseTypeDefinition { - name: string -} - -export interface FieldDefinition { - name: string - getType: Defer - optional: boolean -} - -export interface InterfaceDefinition extends BaseTypeDefinition { - kind: 'InterfaceDefinition' - fields: FieldDefinition[] -} - -export interface TypeAliasDefinition extends BaseTypeDefinition { - kind: 'TypeAliasDefinition' - getType: Defer - isEnum: Defer //If type is UnionType && `types` are scalar strings -} - -export interface TypesMap { - [typeName: string]: TypeDefinition -} - -export interface InterfaceNamesToFile { - [interfaceName: string]: File -} - -export interface ModelField { - fieldName: string - fieldOptional: boolean -} - type ExtractableType = TSTypeAliasDeclaration | TSInterfaceDeclaration -const filesToTypesMap: { [filePath: string]: TypesMap } = {} - -function buildTypeGetter( - type: InternalInnerType, - filePath: string, -): () => InnerAndTypeDefinition { - if (type.kind === 'TypeReferenceAnnotation') { - return () => filesToTypesMap[filePath][type.referenceType] - } else { - return () => type - } -} - -export function isFieldDefinitionEnumOrLiteral( - modelFieldType: InnerAndTypeDefinition, -): boolean { - // If type is: 'value' - if (isLiteralString(modelFieldType)) { - return true - } - - if ( - modelFieldType.kind === 'UnionTypeAnnotation' && - modelFieldType.isEnum() - ) { - return true - } - - // If type is: type X = 'value' - if ( - modelFieldType.kind === 'TypeAliasDefinition' && - isLiteralString(modelFieldType.getType()) - ) { - return true - } - - // If type is: Type X = 'value' | 'value2' +function shouldExtractType(node: Statement) { return ( - modelFieldType.kind === 'TypeAliasDefinition' && modelFieldType.isEnum() + node.type === 'TSTypeAliasDeclaration' || + node.type === 'TSInterfaceDeclaration' ) } -function isLiteralString(type: InnerAndTypeDefinition) { - return type.kind === 'LiteralTypeAnnotation' && type.type === 'string' -} - -export function getEnumValues(type: InnerAndTypeDefinition): string[] { - // If type is: 'value' - if (isLiteralString(type)) { - return [(type as LiteralTypeAnnotation).value as string] - } - - if ( - type.kind === 'TypeAliasDefinition' && - isLiteralString(type.getType()) - ) { - return [(type.getType() as LiteralTypeAnnotation).value as string] - } - - let unionTypes: InnerAndTypeDefinition[] = [] - - if (type.kind === 'TypeAliasDefinition' && type.isEnum()) { - unionTypes = (type.getType() as UnionTypeAnnotation).getTypes() - } else if (type.kind === 'UnionTypeAnnotation' && type.isEnum) { - unionTypes = type.getTypes() - } else { - return [] - } - - return unionTypes.map(unionType => { - return (unionType as LiteralTypeAnnotation).value - }) as string[] -} - -function isEnumUnion(unionTypes: InnerAndTypeDefinition[]) { - return unionTypes.every(unionType => { - return ( - unionType.kind === 'LiteralTypeAnnotation' && - unionType.isArray === false && - unionType.type === 'string' - ) - }) -} - -function createTypeAlias( - name: string, - type: InternalInnerType, - filePath: string, -): TypeAliasDefinition { - return { - kind: 'TypeAliasDefinition', - name, - getType: buildTypeGetter(type, filePath), - isEnum: () => { - return type.kind === 'UnionTypeAnnotation' && isEnumUnion(type.getTypes()) - }, - } -} - -function createInterfaceField( - name: string, - type: InternalInnerType, - filePath: string, - optional: boolean, -): FieldDefinition { - return { - name, - getType: buildTypeGetter(type, filePath), - optional, - } -} - -function createInterface( - name: string, - fields: FieldDefinition[], -): InterfaceDefinition { - return { - kind: 'InterfaceDefinition', - name, - fields, - } -} - -interface TypeAnnotationOpts { - isArray?: boolean - isTypeRef?: boolean - isAny?: boolean -} - -function createTypeAnnotation( - type: Scalar, - options?: TypeAnnotationOpts, -): ScalarTypeAnnotation { - let opts: TypeAnnotationOpts = {} - if (options === undefined) { - opts = { isArray: false, isTypeRef: false, isAny: false } - } else { - opts = { - isArray: options.isArray === undefined ? false : options.isArray, - isAny: options.isAny === undefined ? false : options.isAny, - } - } - - const isArray = opts.isArray === undefined ? false : opts.isArray - - return { - kind: 'ScalarTypeAnnotation', - type, - isArray, - } -} - -function createUnionTypeAnnotation( - types: InternalInnerType[], - filePath: string, -): UnionTypeAnnotation { - const getTypes = () => { - return types.map(unionType => { - return unionType.kind === 'TypeReferenceAnnotation' - ? filesToTypesMap[filePath][unionType.referenceType] - : unionType - }) - } - - return { - kind: 'UnionTypeAnnotation', - getTypes, - isArray: false, - isEnum: () => isEnumUnion(getTypes()), - } -} - -function createAnonymousInterfaceAnnotation( - fields: FieldDefinition[], - isArray: boolean = false, -): AnonymousInterfaceAnnotation { - return { - kind: 'AnonymousInterfaceAnnotation', - fields, - isArray, - } -} - -function createLiteralTypeAnnotation( - type: string, - value: string | number | boolean, - isArray: boolean = false, -): LiteralTypeAnnotation { - return { - kind: 'LiteralTypeAnnotation', - type, - value, - isArray, - } -} - -function createTypeReferenceAnnotation( - referenceType: string, -): TypeReferenceAnnotation { - return { kind: 'TypeReferenceAnnotation', referenceType } -} - -function computeType(node: TSType, filePath: string): InternalInnerType { +export function computeType(node: TSType, filePath: string): InternalInnerType { if (isTSParenthesizedType(node)) { node = node.typeAnnotation } @@ -385,10 +122,6 @@ function computeType(node: TSType, filePath: string): InternalInnerType { return createTypeAnnotation('_UNKNOWN_TYPE_') } -function getLine(node: BaseNode) { - return node.loc === null ? 'unknown' : node.loc.start.line -} - function extractTypeAlias( typeName: string, typeAlias: TSTypeAliasDeclaration, @@ -457,49 +190,6 @@ function extractInterface( return createInterface(typeName, interfaceFields) } -export function buildTypesMap(filePath: string): TypesMap { - if (filesToTypesMap[filePath] !== undefined) { - return filesToTypesMap[filePath] - } - - const file = fs.readFileSync(filePath).toString() - - const ast = parseTS(file, { - plugins: ['typescript'], - sourceType: 'module', - tokens: true, - }) - - const typesMap = findTypescriptTypes(ast).reduce( - (acc, type) => { - const typeName = type.id.name - if (type.type === 'TSTypeAliasDeclaration') { - return { - ...acc, - [typeName]: extractTypeAlias(typeName, type, filePath), - } - } - - return { - ...acc, - [typeName]: extractInterface(typeName, type.body.body, filePath), - } - }, - {} as TypesMap, - ) - - filesToTypesMap[filePath] = typesMap - - return typesMap -} - -function shouldExtractType(node: Statement) { - return ( - node.type === 'TSTypeAliasDeclaration' || - node.type === 'TSInterfaceDeclaration' - ) -} - function findTypescriptTypes(sourceFile: TSFile): ExtractableType[] { const statements = sourceFile.program.body @@ -519,15 +209,6 @@ function findTypescriptTypes(sourceFile: TSFile): ExtractableType[] { return [...types, ...typesFromNamedExport] as ExtractableType[] } -export function findTypeInFile( - filePath: string, - typeName: string, -): TypeDefinition | undefined { - const filesToTypesMap = buildFilesToTypesMap([{ path: filePath }]) - - return filesToTypesMap[filePath][typeName] -} - function isFieldOptional(node: TSPropertySignature) { if (!!node.optional) { return true @@ -551,3 +232,31 @@ function isFieldOptional(node: TSPropertySignature) { return false } + +export function buildTSTypesMap(fileContent: string, filePath: string) { + const ast = parseTS(fileContent, { + plugins: ['typescript'], + sourceType: 'module', + }) + + const typesMap = findTypescriptTypes(ast).reduce( + (acc, type) => { + const typeName = type.id.name + + if (isTSTypeAliasDeclaration(type)) { + return { + ...acc, + [typeName]: extractTypeAlias(typeName, type, filePath), + } + } + + return { + ...acc, + [typeName]: extractInterface(typeName, type.body.body, filePath), + } + }, + {} as TypesMap, + ) + + return typesMap +} diff --git a/packages/graphqlgen/src/introspection/types.ts b/packages/graphqlgen/src/introspection/types.ts new file mode 100644 index 00000000..f0b4ca45 --- /dev/null +++ b/packages/graphqlgen/src/introspection/types.ts @@ -0,0 +1,82 @@ +import { File } from 'graphqlgen-json-schema' + +export type InnerType = + | ScalarTypeAnnotation + | UnionTypeAnnotation + | AnonymousInterfaceAnnotation + | LiteralTypeAnnotation + +export type InternalInnerType = InnerType | TypeReferenceAnnotation + +export type UnknownType = '_UNKNOWN_TYPE_' +export type Scalar = 'string' | 'number' | 'boolean' | null + +export type TypeDefinition = InterfaceDefinition | TypeAliasDefinition + +export type InnerAndTypeDefinition = InnerType | TypeDefinition + +type Defer = () => T + +interface BaseTypeDefinition { + name: string +} + +export interface InterfaceDefinition extends BaseTypeDefinition { + kind: 'InterfaceDefinition' + fields: FieldDefinition[] +} + +export interface FieldDefinition { + name: string + getType: Defer + optional: boolean +} + +export interface TypeAliasDefinition extends BaseTypeDefinition { + kind: 'TypeAliasDefinition' + getType: Defer + isEnum: Defer //If type is UnionType && `types` are scalar strings +} + +export interface UnionTypeAnnotation { + kind: 'UnionTypeAnnotation' + getTypes: Defer + isArray: boolean + isEnum: Defer +} + +export interface ScalarTypeAnnotation { + kind: 'ScalarTypeAnnotation' + type: Scalar | UnknownType + isArray: boolean +} + +export interface AnonymousInterfaceAnnotation { + kind: 'AnonymousInterfaceAnnotation' + fields: FieldDefinition[] + isArray: boolean +} + +export interface TypeReferenceAnnotation { + kind: 'TypeReferenceAnnotation' + referenceType: string +} + +export interface LiteralTypeAnnotation { + kind: 'LiteralTypeAnnotation' + type: string + value: string | number | boolean + isArray: boolean +} + +export interface TypesMap { + [typeName: string]: TypeDefinition +} + +export interface FilesToTypesMap { + [filePath: string]: TypesMap +} + +export interface InterfaceNamesToFile { + [interfaceName: string]: File +} diff --git a/packages/graphqlgen/src/introspection/utils.ts b/packages/graphqlgen/src/introspection/utils.ts new file mode 100644 index 00000000..da62a89b --- /dev/null +++ b/packages/graphqlgen/src/introspection/utils.ts @@ -0,0 +1,93 @@ +import { BaseNode } from '@babel/types' + +import { + InnerAndTypeDefinition, + InternalInnerType, + LiteralTypeAnnotation, + UnionTypeAnnotation, +} from './types' + +import { filesToTypesMap } from './index' + +export function buildTypeGetter( + type: InternalInnerType, + filePath: string, +): () => InnerAndTypeDefinition { + if (type.kind === 'TypeReferenceAnnotation') { + return () => filesToTypesMap[filePath][type.referenceType] + } else { + return () => type + } +} + +export function isFieldDefinitionEnumOrLiteral( + modelFieldType: InnerAndTypeDefinition, +): boolean { + // If type is: 'value' + if (isLiteralString(modelFieldType)) { + return true + } + + if ( + modelFieldType.kind === 'UnionTypeAnnotation' && + modelFieldType.isEnum() + ) { + return true + } + + // If type is: type X = 'value' + if ( + modelFieldType.kind === 'TypeAliasDefinition' && + isLiteralString(modelFieldType.getType()) + ) { + return true + } + + // If type is: Type X = 'value' | 'value2' + return ( + modelFieldType.kind === 'TypeAliasDefinition' && modelFieldType.isEnum() + ) +} + +export function isLiteralString(type: InnerAndTypeDefinition) { + return type.kind === 'LiteralTypeAnnotation' && type.type === 'string' +} + +export function getEnumValues(type: InnerAndTypeDefinition): string[] { + // If type is: 'value' + if (isLiteralString(type)) { + return [(type as LiteralTypeAnnotation).value as string] + } + + if (type.kind === 'TypeAliasDefinition' && isLiteralString(type.getType())) { + return [(type.getType() as LiteralTypeAnnotation).value as string] + } + + let unionTypes: InnerAndTypeDefinition[] = [] + + if (type.kind === 'TypeAliasDefinition' && type.isEnum()) { + unionTypes = (type.getType() as UnionTypeAnnotation).getTypes() + } else if (type.kind === 'UnionTypeAnnotation' && type.isEnum) { + unionTypes = type.getTypes() + } else { + return [] + } + + return unionTypes.map(unionType => { + return (unionType as LiteralTypeAnnotation).value + }) as string[] +} + +export function isEnumUnion(unionTypes: InnerAndTypeDefinition[]) { + return unionTypes.every(unionType => { + return ( + unionType.kind === 'LiteralTypeAnnotation' && + unionType.isArray === false && + unionType.type === 'string' + ) + }) +} + +export function getLine(node: BaseNode) { + return node.loc === null ? 'unknown' : node.loc.start.line +} diff --git a/packages/graphqlgen/src/parse.ts b/packages/graphqlgen/src/parse.ts index 07851654..af9372a4 100644 --- a/packages/graphqlgen/src/parse.ts +++ b/packages/graphqlgen/src/parse.ts @@ -23,11 +23,8 @@ import { extractGraphQLTypesWithoutRootsAndInputsAndEnums, GraphQLTypes, } from './source-helper' -import { buildTypesMap, TypesMap } from './introspection/ts-ast' - -export interface FilesToTypesMap { - [filePath: string]: TypesMap -} +import { FilesToTypesMap } from './introspection/types' +import { buildFilesToTypesMap } from './introspection' export interface NormalizedFile { path: string @@ -149,17 +146,6 @@ export function getDefaultName(file: File): string | null { return file.defaultName || null } -export function buildFilesToTypesMap( - files: NormalizedFile[], -): FilesToTypesMap { - return files.reduce((acc, file) => { - return { - ...acc, - [file.path]: buildTypesMap(file.path), - } - }, {}) -} - export function normalizeFiles( files: File[] | undefined, language: Language, @@ -180,7 +166,7 @@ export function parseModels( ): ModelMap { const graphQLTypes = extractGraphQLTypesWithoutRootsAndInputsAndEnums(schema) const normalizedFiles = normalizeFiles(models.files, language) - const filesToTypesMap = buildFilesToTypesMap(normalizedFiles) + const filesToTypesMap = buildFilesToTypesMap(normalizedFiles, language) const overriddenModels = !!models.override ? models.override : {} const typeToFileMapping = getTypeToFileMapping( normalizedFiles, diff --git a/packages/graphqlgen/src/tests/flow/__snapshots__/basic.test.ts.snap b/packages/graphqlgen/src/tests/flow/__snapshots__/basic.test.ts.snap index cacdb9ff..3ecbb033 100644 --- a/packages/graphqlgen/src/tests/flow/__snapshots__/basic.test.ts.snap +++ b/packages/graphqlgen/src/tests/flow/__snapshots__/basic.test.ts.snap @@ -9,6 +9,8 @@ import type { User } from \\"../../../fixtures/enum/types-flow\\"; type Context = any; +type UserType = \\"ADMIN\\" | \\"EDITOR\\" | \\"COLLABORATOR\\"; + // Types for Query export const Query_defaultResolvers = {}; diff --git a/packages/graphqlgen/src/tests/flow/__snapshots__/large-schema.test.ts.snap b/packages/graphqlgen/src/tests/flow/__snapshots__/large-schema.test.ts.snap index 06648af1..2a3bfa5a 100644 --- a/packages/graphqlgen/src/tests/flow/__snapshots__/large-schema.test.ts.snap +++ b/packages/graphqlgen/src/tests/flow/__snapshots__/large-schema.test.ts.snap @@ -36,6 +36,24 @@ import type { MutationResult } from \\"../../../fixtures/prisma/flow-types\\"; type Context = any; +type PLACE_SIZES = + | \\"ENTIRE_HOUSE\\" + | \\"ENTIRE_APARTMENT\\" + | \\"ENTIRE_EARTH_HOUSE\\" + | \\"ENTIRE_CABIN\\" + | \\"ENTIRE_VILLA\\" + | \\"ENTIRE_PLACE\\" + | \\"ENTIRE_BOAT\\" + | \\"PRIVATE_ROOM\\"; +type CURRENCY = \\"CAD\\" | \\"CHF\\" | \\"EUR\\" | \\"JPY\\" | \\"USD\\" | \\"ZAR\\"; +type PAYMENT_PROVIDER = \\"PAYPAL\\" | \\"CREDIT_CARD\\"; +type NOTIFICATION_TYPE = + | \\"OFFER\\" + | \\"INSTANT_BOOK\\" + | \\"RESPONSIVENESS\\" + | \\"NEW_AMENITIES\\" + | \\"HOUSE_RULES\\"; + // Types for Query export const Query_defaultResolvers = {}; diff --git a/packages/graphqlgen/src/tests/introspection/typescript.test.ts b/packages/graphqlgen/src/tests/introspection/typescript.test.ts index b7773c21..2a7dd9a2 100644 --- a/packages/graphqlgen/src/tests/introspection/typescript.test.ts +++ b/packages/graphqlgen/src/tests/introspection/typescript.test.ts @@ -1,11 +1,14 @@ import { join } from 'path' -import { buildTypesMap } from '../../introspection/ts-ast' +import { buildTypesMap } from '../../introspection/index' const relative = (p: string) => join(__dirname, p) +const language = 'typescript' describe('typescript file introspection', () => { test('find all types in file', () => { - const typesNames = Object.keys(buildTypesMap(relative('./mocks/types.ts'))) + const typesNames = Object.keys( + buildTypesMap(relative('./mocks/types.ts'), language), + ) expect(typesNames).toEqual([ 'Interface', diff --git a/packages/graphqlgen/src/types.ts b/packages/graphqlgen/src/types.ts index bdfeffe0..2168a907 100644 --- a/packages/graphqlgen/src/types.ts +++ b/packages/graphqlgen/src/types.ts @@ -4,7 +4,7 @@ import { GraphQLEnumObject, GraphQLUnionObject, } from './source-helper' -import { TypeDefinition } from './introspection/ts-ast' +import { TypeDefinition } from './introspection/types' export interface GenerateArgs { types: GraphQLTypeObject[] diff --git a/packages/graphqlgen/src/utils.ts b/packages/graphqlgen/src/utils.ts index ce565ddd..43b6ca90 100644 --- a/packages/graphqlgen/src/utils.ts +++ b/packages/graphqlgen/src/utils.ts @@ -2,8 +2,8 @@ import * as path from 'path' import { Language } from 'graphqlgen-json-schema' import { getExtNameFromLanguage } from './path-helpers' -import { InterfaceNamesToFile } from './introspection/ts-ast' -import { FilesToTypesMap, NormalizedFile } from './parse' +import { NormalizedFile } from './parse' +import { FilesToTypesMap, InterfaceNamesToFile } from './introspection/types' export function upperFirst(s: string) { return s.replace(/^\w/, c => c.toUpperCase()) diff --git a/packages/graphqlgen/src/validation.ts b/packages/graphqlgen/src/validation.ts index 5810f7a0..aee598b4 100644 --- a/packages/graphqlgen/src/validation.ts +++ b/packages/graphqlgen/src/validation.ts @@ -1,7 +1,6 @@ import chalk from 'chalk' import { existsSync } from 'fs' import { GraphQLGenDefinition, Language, Models } from 'graphqlgen-json-schema' -import { findTypeInFile } from './introspection/ts-ast' import { outputDefinitionFilesNotFound, outputInterfaceDefinitionsNotFound, @@ -19,9 +18,9 @@ import { getPath, getDefaultName, normalizeFiles, - buildFilesToTypesMap, NormalizedFile, } from './parse' +import { buildFilesToTypesMap, findTypeInFile } from './introspection' type Definition = { typeName: string @@ -181,7 +180,7 @@ function validateSchemaToModelMapping( def => def.definition.typeName, ) - const filesToTypesMap = buildFilesToTypesMap(normalizedFiles) + const filesToTypesMap = buildFilesToTypesMap(normalizedFiles, language) const interfaceNamesToPath = getTypeToFileMapping( normalizedFiles, filesToTypesMap, @@ -222,7 +221,10 @@ function validateSchemaToModelMapping( return true } -export function maybeReplaceDefaultName(typeName: string, defaultName?: string | null) { +export function maybeReplaceDefaultName( + typeName: string, + defaultName?: string | null, +) { return defaultName ? replaceVariablesInString(defaultName, { typeName }) : typeName @@ -265,7 +267,7 @@ export function validateDefinition( return validation } - if (!findTypeInFile(normalizedFilePath, modelName)) { + if (!findTypeInFile(normalizedFilePath, modelName, language)) { validation.interfaceExists = false } From bd8e5600d7dcc31041a2ee4503c36cad94fd665a Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Mon, 5 Nov 2018 01:29:41 +0100 Subject: [PATCH 07/20] Fixed flow export scaffold --- .../src/generators/flow-scaffolder.ts | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/packages/graphqlgen/src/generators/flow-scaffolder.ts b/packages/graphqlgen/src/generators/flow-scaffolder.ts index e0115002..6c76aa90 100644 --- a/packages/graphqlgen/src/generators/flow-scaffolder.ts +++ b/packages/graphqlgen/src/generators/flow-scaffolder.ts @@ -33,6 +33,29 @@ function renderParentResolvers(type: GraphQLTypeObject): CodeFileLike { code, } } +function renderExports(types: GraphQLTypeObject[]): string { + return `\ + // @flow + // This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. + // Please do not import this file directly but copy & paste to your application code. + + import type { Resolvers } from '[TEMPLATE-INTERFACES-PATH]' + ${types + .filter(type => type.type.isObject) + .map( + type => ` + import { ${type.name} } from './${type.name}' + `, + ) + .join(';')} + + export const resolvers: Resolvers = { + ${types + .filter(type => type.type.isObject) + .map(type => `${type.name}`) + .join(',')} + }` +} function renderResolvers( type: GraphQLTypeObject, args: GenerateArgs, @@ -81,20 +104,7 @@ export function generate(args: GenerateArgs): CodeFileLike[] { files.push({ path: 'index.js', force: false, - code: `/* @flow */ - import type { Resolvers } from '[TEMPLATE-INTERFACES-PATH]' - ${args.types - .map( - type => ` - import { ${type.name} } from './${type.name}' - `, - ) - .join(';')} - - export const resolvers: Resolvers = { - ${args.types.map(type => `${type.name}`).join(',')} - } - `, + code: renderExports(args.types) }) return files From e9a87f328a0aee5deb225a0ffa4196a21365091d Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Mon, 5 Nov 2018 01:30:22 +0100 Subject: [PATCH 08/20] Added support for `enums` in typescript --- .../graphqlgen/src/introspection/ts-ast.ts | 46 ++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/packages/graphqlgen/src/introspection/ts-ast.ts b/packages/graphqlgen/src/introspection/ts-ast.ts index 167a16a5..2b287fb4 100644 --- a/packages/graphqlgen/src/introspection/ts-ast.ts +++ b/packages/graphqlgen/src/introspection/ts-ast.ts @@ -24,6 +24,8 @@ import { isTSLiteralType, isTSUnionType, isTSTypeAliasDeclaration, + TSEnumDeclaration, + isTSEnumDeclaration, } from '@babel/types' import { InterfaceDefinition, @@ -47,12 +49,16 @@ import chalk from 'chalk' // /!\ If you add a supported type of field, make sure you update isSupportedField() as well type SupportedFields = TSPropertySignature -type ExtractableType = TSTypeAliasDeclaration | TSInterfaceDeclaration +type ExtractableType = + | TSTypeAliasDeclaration + | TSInterfaceDeclaration + | TSEnumDeclaration function shouldExtractType(node: Statement) { return ( node.type === 'TSTypeAliasDeclaration' || - node.type === 'TSInterfaceDeclaration' + node.type === 'TSInterfaceDeclaration' || + node.type === 'TSEnumDeclaration' ) } @@ -140,6 +146,35 @@ function extractTypeAlias( } } +// Enums are converted to TypeAlias of UnionType +// enum Enum { A, B, C } => type Enum = 'A' | 'B' | 'C' +function extractEnum( + enumName: string, + enumType: TSEnumDeclaration, + filePath: string, +): TypeAliasDefinition { + if ( + enumType.members.some(enumMember => enumMember.id.type === 'StringLiteral') + ) { + throw new Error( + `ERROR: Enum initializers not supported (${enumName} in ${filePath})`, + ) + } + + const enumValuesAsLiteralStrings = enumType.members.map(enumMember => { + return createLiteralTypeAnnotation( + 'string', + (enumMember.id as Identifier).name, + ) + }) + const unionType = createUnionTypeAnnotation( + enumValuesAsLiteralStrings, + filePath, + ) + + return createTypeAlias(enumName, unionType, filePath) +} + function isSupportedTypeOfField(field: TSTypeElement) { return isTSPropertySignature(field) } @@ -250,6 +285,13 @@ export function buildTSTypesMap(fileContent: string, filePath: string) { } } + if (isTSEnumDeclaration(type)) { + return { + ...acc, + [typeName]: extractEnum(typeName, type, filePath), + } + } + return { ...acc, [typeName]: extractInterface(typeName, type.body.body, filePath), From 3d6f9ade8cee94750aa9c957733f011f60a51b9a Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Mon, 5 Nov 2018 01:38:57 +0100 Subject: [PATCH 09/20] Updated tests --- .../src/generators/flow-scaffolder.ts | 18 ++--- .../src/tests/fixtures/enum/schema.graphql | 14 ++-- .../src/tests/fixtures/enum/types-flow.js | 6 +- .../src/tests/fixtures/enum/types.ts | 11 ++- .../flow/__snapshots__/basic.test.ts.snap | 69 ++++++++++++++----- .../__snapshots__/large-schema.test.ts.snap | 13 ++-- .../src/tests/introspection/mocks/types.ts | 3 +- .../tests/introspection/typescript.test.ts | 3 +- .../__snapshots__/basic.test.ts.snap | 35 +++++++--- 9 files changed, 118 insertions(+), 54 deletions(-) diff --git a/packages/graphqlgen/src/generators/flow-scaffolder.ts b/packages/graphqlgen/src/generators/flow-scaffolder.ts index 6c76aa90..190ddce7 100644 --- a/packages/graphqlgen/src/generators/flow-scaffolder.ts +++ b/packages/graphqlgen/src/generators/flow-scaffolder.ts @@ -41,19 +41,19 @@ function renderExports(types: GraphQLTypeObject[]): string { import type { Resolvers } from '[TEMPLATE-INTERFACES-PATH]' ${types - .filter(type => type.type.isObject) - .map( - type => ` + .filter(type => type.type.isObject) + .map( + type => ` import { ${type.name} } from './${type.name}' `, - ) - .join(';')} + ) + .join(';')} export const resolvers: Resolvers = { ${types - .filter(type => type.type.isObject) - .map(type => `${type.name}`) - .join(',')} + .filter(type => type.type.isObject) + .map(type => `${type.name}`) + .join(',')} }` } function renderResolvers( @@ -104,7 +104,7 @@ export function generate(args: GenerateArgs): CodeFileLike[] { files.push({ path: 'index.js', force: false, - code: renderExports(args.types) + code: renderExports(args.types), }) return files diff --git a/packages/graphqlgen/src/tests/fixtures/enum/schema.graphql b/packages/graphqlgen/src/tests/fixtures/enum/schema.graphql index 0ba34c97..d10b3e9e 100644 --- a/packages/graphqlgen/src/tests/fixtures/enum/schema.graphql +++ b/packages/graphqlgen/src/tests/fixtures/enum/schema.graphql @@ -1,15 +1,21 @@ type User { id: ID! name: String! - type: UserType! + enumAnnotation: EnumAnnotation! + enumAsUnionType: EnumAsUnionType! } -enum UserType { - ADMIN +enum EnumAnnotation { EDITOR COLLABORATOR } +enum EnumAsUnionType { + RED + GREEN + BLUE +} + type Query { - createUser(name: String!, type: UserType!): User + createUser(name: String!, type: EnumAnnotation!): User } diff --git a/packages/graphqlgen/src/tests/fixtures/enum/types-flow.js b/packages/graphqlgen/src/tests/fixtures/enum/types-flow.js index 8b1371f0..bcae1e87 100644 --- a/packages/graphqlgen/src/tests/fixtures/enum/types-flow.js +++ b/packages/graphqlgen/src/tests/fixtures/enum/types-flow.js @@ -3,7 +3,9 @@ export interface User { id: string, name: string, - type: UserType, + enumAnnotation: EnumAnnotation, + enumAsUnionType: EnumAsUnionType, } -type UserType = 'ADMIN' | 'EDITOR' | 'COLLABORATOR' +type EnumAnnotation = 'ADMIN' | 'EDITOR' | 'COLLABORATOR' +type EnumAsUnionType = 'RED' | 'GREEN' | 'BLUE' diff --git a/packages/graphqlgen/src/tests/fixtures/enum/types.ts b/packages/graphqlgen/src/tests/fixtures/enum/types.ts index a2c360b9..091a2f02 100644 --- a/packages/graphqlgen/src/tests/fixtures/enum/types.ts +++ b/packages/graphqlgen/src/tests/fixtures/enum/types.ts @@ -1,7 +1,14 @@ export interface User { id: string name: string - type: UserType + enumAnnotation: EnumAnnotation + enumAsUnionType: EnumAsUnionType } -type UserType = 'ADMIN' | 'EDITOR' | 'COLLABORATOR' +enum EnumAnnotation { + ADMIN, + EDITOR, + COLLABORATOR, +} + +type EnumAsUnionType = 'RED' | 'GREEN' | 'BLUE' diff --git a/packages/graphqlgen/src/tests/flow/__snapshots__/basic.test.ts.snap b/packages/graphqlgen/src/tests/flow/__snapshots__/basic.test.ts.snap index 3ecbb033..a9f7d4f2 100644 --- a/packages/graphqlgen/src/tests/flow/__snapshots__/basic.test.ts.snap +++ b/packages/graphqlgen/src/tests/flow/__snapshots__/basic.test.ts.snap @@ -9,14 +9,15 @@ import type { User } from \\"../../../fixtures/enum/types-flow\\"; type Context = any; -type UserType = \\"ADMIN\\" | \\"EDITOR\\" | \\"COLLABORATOR\\"; +type EnumAnnotation = \\"EDITOR\\" | \\"COLLABORATOR\\"; +type EnumAsUnionType = \\"RED\\" | \\"GREEN\\" | \\"BLUE\\"; // Types for Query export const Query_defaultResolvers = {}; export interface Query_Args_CreateUser { name: string; - type: UserType; + type: EnumAnnotation; } export type Query_CreateUser_Resolver = ( @@ -39,7 +40,7 @@ export interface Query_Resolvers { export const User_defaultResolvers = { id: (parent: User) => parent.id, name: (parent: User) => parent.name, - type: (parent: User) => parent.type + enumAsUnionType: (parent: User) => parent.enumAsUnionType }; export type User_Id_Resolver = ( @@ -56,12 +57,19 @@ export type User_Name_Resolver = ( info: GraphQLResolveInfo ) => string | Promise; -export type User_Type_Resolver = ( +export type User_EnumAnnotation_Resolver = ( + parent: User, + args: {}, + ctx: Context, + info: GraphQLResolveInfo +) => EnumAnnotation | Promise; + +export type User_EnumAsUnionType_Resolver = ( parent: User, args: {}, ctx: Context, info: GraphQLResolveInfo -) => UserType | Promise; +) => EnumAsUnionType | Promise; export interface User_Resolvers { id: ( @@ -78,12 +86,19 @@ export interface User_Resolvers { info: GraphQLResolveInfo ) => string | Promise; - type: ( + enumAnnotation: ( parent: User, args: {}, ctx: Context, info: GraphQLResolveInfo - ) => UserType | Promise; + ) => EnumAnnotation | Promise; + + enumAsUnionType: ( + parent: User, + args: {}, + ctx: Context, + info: GraphQLResolveInfo + ) => EnumAsUnionType | Promise; } export interface Resolvers { @@ -101,7 +116,11 @@ import { User_defaultResolvers } from \\"[TEMPLATE-INTERFACES-PATH]\\"; import type { User_Resolvers } from \\"[TEMPLATE-INTERFACES-PATH]\\"; export const User: User_Resolvers = { - ...User_defaultResolvers + ...User_defaultResolvers, + + enumAnnotation: (parent, args, ctx, info) => { + throw new Error(\\"Resolver not implemented\\"); + } }; ", "force": false, @@ -121,16 +140,17 @@ export const Query: Query_Resolvers = { "path": "Query.js", }, Object { - "code": "/* @flow */ + "code": "// @flow +// This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. +// Please do not import this file directly but copy & paste to your application code. + import type { Resolvers } from \\"[TEMPLATE-INTERFACES-PATH]\\"; import { Query } from \\"./Query\\"; -import { UserType } from \\"./UserType\\"; import { User } from \\"./User\\"; export const resolvers: Resolvers = { Query, - UserType, User }; ", @@ -233,16 +253,17 @@ export const Mutation: Mutation_Resolvers = { "path": "Mutation.js", }, Object { - "code": "/* @flow */ + "code": "// @flow +// This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. +// Please do not import this file directly but copy & paste to your application code. + import type { Resolvers } from \\"[TEMPLATE-INTERFACES-PATH]\\"; import { Mutation } from \\"./Mutation\\"; -import { AddMemberData } from \\"./AddMemberData\\"; import { AddMemberPayload } from \\"./AddMemberPayload\\"; export const resolvers: Resolvers = { Mutation, - AddMemberData, AddMemberPayload }; ", @@ -571,7 +592,10 @@ export const Query: Query_Resolvers = { "path": "Query.js", }, Object { - "code": "/* @flow */ + "code": "// @flow +// This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. +// Please do not import this file directly but copy & paste to your application code. + import type { Resolvers } from \\"[TEMPLATE-INTERFACES-PATH]\\"; import { Query } from \\"./Query\\"; @@ -742,7 +766,10 @@ export const Professor: Professor_Resolvers = { "path": "Professor.js", }, Object { - "code": "/* @flow */ + "code": "// @flow +// This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. +// Please do not import this file directly but copy & paste to your application code. + import type { Resolvers } from \\"[TEMPLATE-INTERFACES-PATH]\\"; import { User } from \\"./User\\"; @@ -845,7 +872,10 @@ export const Query: Query_Resolvers = { "path": "Query.js", }, Object { - "code": "/* @flow */ + "code": "// @flow +// This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. +// Please do not import this file directly but copy & paste to your application code. + import type { Resolvers } from \\"[TEMPLATE-INTERFACES-PATH]\\"; import { Query } from \\"./Query\\"; @@ -1181,7 +1211,10 @@ export const Query: Query_Resolvers = { "path": "Query.js", }, Object { - "code": "/* @flow */ + "code": "// @flow +// This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. +// Please do not import this file directly but copy & paste to your application code. + import type { Resolvers } from \\"[TEMPLATE-INTERFACES-PATH]\\"; import { Query } from \\"./Query\\"; diff --git a/packages/graphqlgen/src/tests/flow/__snapshots__/large-schema.test.ts.snap b/packages/graphqlgen/src/tests/flow/__snapshots__/large-schema.test.ts.snap index 2a3bfa5a..0e0a6f52 100644 --- a/packages/graphqlgen/src/tests/flow/__snapshots__/large-schema.test.ts.snap +++ b/packages/graphqlgen/src/tests/flow/__snapshots__/large-schema.test.ts.snap @@ -4234,7 +4234,10 @@ export const Mutation: Mutation_Resolvers = { "path": "Mutation.js", }, Object { - "code": "/* @flow */ + "code": "// @flow +// This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. +// Please do not import this file directly but copy & paste to your application code. + import type { Resolvers } from \\"[TEMPLATE-INTERFACES-PATH]\\"; import { Query } from \\"./Query\\"; @@ -4252,21 +4255,17 @@ import { Viewer } from \\"./Viewer\\"; import { User } from \\"./User\\"; import { Booking } from \\"./Booking\\"; import { Place } from \\"./Place\\"; -import { PLACE_SIZES } from \\"./PLACE_SIZES\\"; import { Amenities } from \\"./Amenities\\"; import { Pricing } from \\"./Pricing\\"; -import { CURRENCY } from \\"./CURRENCY\\"; import { PlaceViews } from \\"./PlaceViews\\"; import { GuestRequirements } from \\"./GuestRequirements\\"; import { Policies } from \\"./Policies\\"; import { HouseRules } from \\"./HouseRules\\"; import { Payment } from \\"./Payment\\"; import { PaymentAccount } from \\"./PaymentAccount\\"; -import { PAYMENT_PROVIDER } from \\"./PAYMENT_PROVIDER\\"; import { PaypalInformation } from \\"./PaypalInformation\\"; import { CreditCardInformation } from \\"./CreditCardInformation\\"; import { Notification } from \\"./Notification\\"; -import { NOTIFICATION_TYPE } from \\"./NOTIFICATION_TYPE\\"; import { Message } from \\"./Message\\"; import { Mutation } from \\"./Mutation\\"; import { AuthPayload } from \\"./AuthPayload\\"; @@ -4288,21 +4287,17 @@ export const resolvers: Resolvers = { User, Booking, Place, - PLACE_SIZES, Amenities, Pricing, - CURRENCY, PlaceViews, GuestRequirements, Policies, HouseRules, Payment, PaymentAccount, - PAYMENT_PROVIDER, PaypalInformation, CreditCardInformation, Notification, - NOTIFICATION_TYPE, Message, Mutation, AuthPayload, diff --git a/packages/graphqlgen/src/tests/introspection/mocks/types.ts b/packages/graphqlgen/src/tests/introspection/mocks/types.ts index 0b9d158b..dbce4c8c 100644 --- a/packages/graphqlgen/src/tests/introspection/mocks/types.ts +++ b/packages/graphqlgen/src/tests/introspection/mocks/types.ts @@ -18,7 +18,8 @@ export type ExportedType = { field: string } -export enum EnumShouldNotBeEvaluated { +// @ts-ignore +enum Enum { A, B, C } diff --git a/packages/graphqlgen/src/tests/introspection/typescript.test.ts b/packages/graphqlgen/src/tests/introspection/typescript.test.ts index 2a7dd9a2..91e51bcf 100644 --- a/packages/graphqlgen/src/tests/introspection/typescript.test.ts +++ b/packages/graphqlgen/src/tests/introspection/typescript.test.ts @@ -1,5 +1,5 @@ import { join } from 'path' -import { buildTypesMap } from '../../introspection/index' +import { buildTypesMap } from '../../introspection' const relative = (p: string) => join(__dirname, p) const language = 'typescript' @@ -13,6 +13,7 @@ describe('typescript file introspection', () => { expect(typesNames).toEqual([ 'Interface', 'Type', + 'Enum', 'ExportedInterface', 'ExportedType', ]) diff --git a/packages/graphqlgen/src/tests/typescript/__snapshots__/basic.test.ts.snap b/packages/graphqlgen/src/tests/typescript/__snapshots__/basic.test.ts.snap index 493e069c..65ce5335 100644 --- a/packages/graphqlgen/src/tests/typescript/__snapshots__/basic.test.ts.snap +++ b/packages/graphqlgen/src/tests/typescript/__snapshots__/basic.test.ts.snap @@ -8,14 +8,15 @@ import { User } from \\"../../../fixtures/enum/types\\"; type Context = any; -type UserType = \\"ADMIN\\" | \\"EDITOR\\" | \\"COLLABORATOR\\"; +type EnumAnnotation = \\"EDITOR\\" | \\"COLLABORATOR\\"; +type EnumAsUnionType = \\"RED\\" | \\"GREEN\\" | \\"BLUE\\"; export namespace QueryResolvers { export const defaultResolvers = {}; export interface ArgsCreateUser { name: string; - type: UserType; + type: EnumAnnotation; } export type CreateUserResolver = ( @@ -39,7 +40,7 @@ export namespace UserResolvers { export const defaultResolvers = { id: (parent: User) => parent.id, name: (parent: User) => parent.name, - type: (parent: User) => parent.type + enumAsUnionType: (parent: User) => parent.enumAsUnionType }; export type IdResolver = ( @@ -56,12 +57,19 @@ export namespace UserResolvers { info: GraphQLResolveInfo ) => string | Promise; - export type TypeResolver = ( + export type EnumAnnotationResolver = ( parent: User, args: {}, ctx: Context, info: GraphQLResolveInfo - ) => UserType | Promise; + ) => EnumAnnotation | Promise; + + export type EnumAsUnionTypeResolver = ( + parent: User, + args: {}, + ctx: Context, + info: GraphQLResolveInfo + ) => EnumAsUnionType | Promise; export interface Type { id: ( @@ -78,12 +86,19 @@ export namespace UserResolvers { info: GraphQLResolveInfo ) => string | Promise; - type: ( + enumAnnotation: ( parent: User, args: {}, ctx: Context, info: GraphQLResolveInfo - ) => UserType | Promise; + ) => EnumAnnotation | Promise; + + enumAsUnionType: ( + parent: User, + args: {}, + ctx: Context, + info: GraphQLResolveInfo + ) => EnumAsUnionType | Promise; } } @@ -103,7 +118,11 @@ Array [ import { UserResolvers } from \\"[TEMPLATE-INTERFACES-PATH]\\"; export const User: UserResolvers.Type = { - ...UserResolvers.defaultResolvers + ...UserResolvers.defaultResolvers, + + enumAnnotation: parent => { + throw new Error(\\"Resolver not implemented\\"); + } }; ", "force": false, From 6eb4f0db7a21b305d8f235b8bdc43ea2ffe4a314 Mon Sep 17 00:00:00 2001 From: schickling Date: Sun, 4 Nov 2018 16:50:18 -0800 Subject: [PATCH 10/20] Released 0.3.0-beta1 --- packages/graphqlgen/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graphqlgen/package.json b/packages/graphqlgen/package.json index 351f8030..b138dc68 100644 --- a/packages/graphqlgen/package.json +++ b/packages/graphqlgen/package.json @@ -1,6 +1,6 @@ { "name": "graphqlgen", - "version": "0.2.14", + "version": "0.3.0-beta1", "description": "Generate resolver types based on a GraphQL Schema", "main": "dist/index.js", "files": [ From 568ceac69ee8d0e90beeea527cf4768985e8cf97 Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Mon, 5 Nov 2018 22:11:30 +0100 Subject: [PATCH 11/20] Review fixes --- packages/graphqlgen/src/generators/common.ts | 2 +- packages/graphqlgen/src/generators/ts-generator.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/graphqlgen/src/generators/common.ts b/packages/graphqlgen/src/generators/common.ts index c73d59e1..d959b797 100644 --- a/packages/graphqlgen/src/generators/common.ts +++ b/packages/graphqlgen/src/generators/common.ts @@ -150,7 +150,7 @@ function shouldRenderDefaultResolver( return false } - if (modelField == undefined) { + if (modelField === undefined) { return false } diff --git a/packages/graphqlgen/src/generators/ts-generator.ts b/packages/graphqlgen/src/generators/ts-generator.ts index 50d82fe3..a0dffdef 100644 --- a/packages/graphqlgen/src/generators/ts-generator.ts +++ b/packages/graphqlgen/src/generators/ts-generator.ts @@ -103,7 +103,6 @@ function renderHeader(args: GenerateArgs): string { import { GraphQLResolveInfo } from 'graphql' ${modelImports} - ${renderContext(args.context)} ` } From 8f231c002a07898f4846b4adbd3241815f64bb99 Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Mon, 5 Nov 2018 22:11:59 +0100 Subject: [PATCH 12/20] Scoped flow input types --- packages/graphqlgen/src/generators/flow-generator.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/graphqlgen/src/generators/flow-generator.ts b/packages/graphqlgen/src/generators/flow-generator.ts index 346731ad..586e5271 100644 --- a/packages/graphqlgen/src/generators/flow-generator.ts +++ b/packages/graphqlgen/src/generators/flow-generator.ts @@ -161,7 +161,9 @@ function renderInputTypeInterfaces( return getDistinctInputTypes(type, typeToInputTypeAssociation, inputTypesMap) .map(typeAssociation => { - return `export interface ${inputTypesMap[typeAssociation].name} { + return `export interface ${upperFirst(type.name)}_${upperFirst( + inputTypesMap[typeAssociation].name, + )} { ${inputTypesMap[typeAssociation].fields.map( field => `${field.name}: ${printFieldLikeType(field, modelMap)}`, )} From 67b6a4da58a16c9876b0e1e529a058dc9951c12c Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Mon, 5 Nov 2018 22:16:12 +0100 Subject: [PATCH 13/20] Defined unhandled types as unknown and removed console.log/throws --- .../graphqlgen/src/introspection/flow-ast.ts | 94 +++++++------------ .../graphqlgen/src/introspection/ts-ast.ts | 47 ++++------ .../graphqlgen/src/introspection/utils.ts | 6 -- 3 files changed, 49 insertions(+), 98 deletions(-) diff --git a/packages/graphqlgen/src/introspection/flow-ast.ts b/packages/graphqlgen/src/introspection/flow-ast.ts index 6fd9f07f..c2f9bbef 100644 --- a/packages/graphqlgen/src/introspection/flow-ast.ts +++ b/packages/graphqlgen/src/introspection/flow-ast.ts @@ -1,5 +1,3 @@ -import * as fs from 'fs' - import { parse as parseFlow } from '@babel/parser' import { ExportNamedDeclaration, @@ -28,9 +26,7 @@ import { isUnionTypeAnnotation, isNullLiteralTypeAnnotation, } from '@babel/types' -import { File } from 'graphqlgen-json-schema' -import { getPath } from '../parse' import { TypeAliasDefinition, InterfaceDefinition, @@ -47,19 +43,9 @@ import { createAnonymousInterfaceAnnotation, createUnionTypeAnnotation, } from './factory' -import { getLine } from './utils' -import chalk from 'chalk' -// /!\ If you add a supported type of field, make sure you update isSupportedField() as well -type SupportedFields = ObjectTypeProperty type ExtractableType = TypeAlias | InterfaceDeclaration -function getSourceFile(filePath: string): FlowFile { - const file = fs.readFileSync(filePath).toString() - - return parseFlow(file, { plugins: ['flow'] }) -} - function shouldExtractType(node: Statement) { return node.type === 'TypeAlias' || node.type === 'InterfaceDeclaration' } @@ -83,13 +69,6 @@ function findFlowTypes(sourceFile: FlowFile): ExtractableType[] { return [...types, ...typesFromNamedExport] as ExtractableType[] } -export function typeNamesFromFlowFile(file: File): string[] { - const filePath = getPath(file) - const sourceFile = getSourceFile(filePath) - - return findFlowTypes(sourceFile).map(node => node.id.name) -} - export function isFieldOptional(node: ObjectTypeProperty) { if (!!node.optional) { return true @@ -108,29 +87,6 @@ export function isFieldOptional(node: ObjectTypeProperty) { return false } -function isSupportedTypeOfField( - field: ObjectTypeProperty | ObjectTypeSpreadProperty, -) { - return isObjectTypeProperty(field) -} - -function throwIfUnsupportedFields( - fields: (ObjectTypeProperty | ObjectTypeSpreadProperty)[], - filePath: string, -) { - const unsupportedFields = fields.filter( - field => !isSupportedTypeOfField(field), - ) - - if (unsupportedFields.length > 0) { - throw new Error( - `Unsupported notation for fields: ${unsupportedFields - .map(field => `Line ${getLine(field)} in ${filePath}`) - .join(', ')}`, - ) - } -} - export function computeType( node: FlowType, filePath: string, @@ -191,14 +147,6 @@ export function computeType( return createUnionTypeAnnotation(unionTypes, filePath) } - console.log( - chalk.yellow( - `WARNING: Unsupported type ${node.type} (Line ${getLine( - node, - )} in ${filePath}). Please file an issue at https://github.com/prisma/graphqlgen/issues`, - ), - ) - return createTypeAnnotation('_UNKNOWN_TYPE_') } @@ -216,20 +164,43 @@ function extractTypeAlias( } } +function isSupportedTypeOfField( + field: ObjectTypeProperty | ObjectTypeSpreadProperty, +) { + return isObjectTypeProperty(field) +} + +function extractInterfaceFieldName( + field: ObjectTypeProperty | ObjectTypeSpreadProperty, +): string { + if (isObjectTypeProperty(field)) { + return field.key.type === 'Identifier' + ? (field.key as Identifier).name + : (field.key as StringLiteral).value + } + + return '' +} + function extractInterfaceFields( fields: (ObjectTypeProperty | ObjectTypeSpreadProperty)[], filePath: string, ) { - throwIfUnsupportedFields(fields, filePath) - - return (fields as SupportedFields[]).map(field => { - const fieldName = - field.key.type === 'Identifier' - ? (field.key as Identifier).name - : (field.key as StringLiteral).value + return fields.map(field => { + const fieldName = extractInterfaceFieldName(field) + + if (!isSupportedTypeOfField(field)) { + return createInterfaceField( + '', + createTypeAnnotation('_UNKNOWN_TYPE_'), + filePath, + false, + ) + } - const fieldType = computeType(field.value, filePath) - const isOptional = isFieldOptional(field) + const fieldAsObjectTypeProperty = field as ObjectTypeProperty + const fieldType = computeType(fieldAsObjectTypeProperty.value, filePath) + const isOptional = isFieldOptional(fieldAsObjectTypeProperty) return createInterfaceField(fieldName, fieldType, filePath, isOptional) }) @@ -248,6 +219,7 @@ function extractInterface( export function buildFlowTypesMap(fileContent: string, filePath: string) { const ast = parseFlow(fileContent, { plugins: ['flow'], + sourceType: 'module', }) const typesMap = findFlowTypes(ast).reduce( diff --git a/packages/graphqlgen/src/introspection/ts-ast.ts b/packages/graphqlgen/src/introspection/ts-ast.ts index 2b287fb4..da092dd4 100644 --- a/packages/graphqlgen/src/introspection/ts-ast.ts +++ b/packages/graphqlgen/src/introspection/ts-ast.ts @@ -43,8 +43,6 @@ import { createAnonymousInterfaceAnnotation, createUnionTypeAnnotation, } from './factory' -import { getLine } from './utils' -import chalk from 'chalk' // /!\ If you add a supported type of field, make sure you update isSupportedField() as well type SupportedFields = TSPropertySignature @@ -117,14 +115,6 @@ export function computeType(node: TSType, filePath: string): InternalInnerType { return createUnionTypeAnnotation(unionTypes, filePath) } - console.log( - chalk.yellow( - `WARNING: Unsupported type ${node.type} (Line ${getLine( - node, - )} in ${filePath}). Please file an issue at https://github.com/prisma/graphqlgen/issues`, - ), - ) - return createTypeAnnotation('_UNKNOWN_TYPE_') } @@ -157,7 +147,7 @@ function extractEnum( enumType.members.some(enumMember => enumMember.id.type === 'StringLiteral') ) { throw new Error( - `ERROR: Enum initializers not supported (${enumName} in ${filePath})`, + `ERROR: Enum initializers not supported (${enumName} in ${filePath}).`, ) } @@ -179,29 +169,24 @@ function isSupportedTypeOfField(field: TSTypeElement) { return isTSPropertySignature(field) } -function throwIfUnsupportedFields(fields: TSTypeElement[], filePath: string) { - const unsupportedFields = fields.filter( - field => !isSupportedTypeOfField(field), - ) - - if (unsupportedFields.length > 0) { - throw new Error( - `Unsupported notation for fields: ${unsupportedFields - .map(field => `Line ${getLine(field)} in ${filePath}`) - .join(', ')}`, - ) +function extractInterfaceFieldName(field: TSTypeElement): string { + if (isTSPropertySignature(field)) { + return (field.key as Identifier).name } + + return '' } function extractInterfaceFields(fields: TSTypeElement[], filePath: string) { - throwIfUnsupportedFields(fields, filePath) - - return (fields as SupportedFields[]).map(field => { - const fieldName = (field.key as Identifier).name - - if (!field.typeAnnotation) { - throw new Error( - `ERROR: Unsupported notation (Line ${getLine(field)} in ${filePath})`, + return fields.map(field => { + const fieldName = extractInterfaceFieldName(field) + + if (!isSupportedTypeOfField(field) || !field.typeAnnotation) { + return createInterfaceField( + '', + createTypeAnnotation('_UNKNOWN_TYPE_'), + filePath, + false, ) } @@ -209,7 +194,7 @@ function extractInterfaceFields(fields: TSTypeElement[], filePath: string) { field.typeAnnotation!.typeAnnotation, filePath, ) - const isOptional = isFieldOptional(field) + const isOptional = isFieldOptional(field as TSPropertySignature) return createInterfaceField(fieldName, fieldType, filePath, isOptional) }) diff --git a/packages/graphqlgen/src/introspection/utils.ts b/packages/graphqlgen/src/introspection/utils.ts index da62a89b..86c23e65 100644 --- a/packages/graphqlgen/src/introspection/utils.ts +++ b/packages/graphqlgen/src/introspection/utils.ts @@ -1,5 +1,3 @@ -import { BaseNode } from '@babel/types' - import { InnerAndTypeDefinition, InternalInnerType, @@ -87,7 +85,3 @@ export function isEnumUnion(unionTypes: InnerAndTypeDefinition[]) { ) }) } - -export function getLine(node: BaseNode) { - return node.loc === null ? 'unknown' : node.loc.start.line -} From cc1fd52bd61bf266fb92a51b35a55c55f4c05b7e Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Mon, 5 Nov 2018 22:16:21 +0100 Subject: [PATCH 14/20] Updated tests --- .../flow/__snapshots__/basic.test.ts.snap | 2 +- .../src/tests/introspection/flow.test.ts | 24 ++++--------------- .../__snapshots__/basic.test.ts.snap | 7 ------ .../__snapshots__/large-schema.test.ts.snap | 1 - 4 files changed, 6 insertions(+), 28 deletions(-) diff --git a/packages/graphqlgen/src/tests/flow/__snapshots__/basic.test.ts.snap b/packages/graphqlgen/src/tests/flow/__snapshots__/basic.test.ts.snap index a9f7d4f2..3563eb40 100644 --- a/packages/graphqlgen/src/tests/flow/__snapshots__/basic.test.ts.snap +++ b/packages/graphqlgen/src/tests/flow/__snapshots__/basic.test.ts.snap @@ -172,7 +172,7 @@ type Context = any; // Types for Mutation export const Mutation_defaultResolvers = {}; -export interface AddMemberData { +export interface Mutation_AddMemberData { email: string; projects: string[]; } diff --git a/packages/graphqlgen/src/tests/introspection/flow.test.ts b/packages/graphqlgen/src/tests/introspection/flow.test.ts index cfd64dbf..120f6e4d 100644 --- a/packages/graphqlgen/src/tests/introspection/flow.test.ts +++ b/packages/graphqlgen/src/tests/introspection/flow.test.ts @@ -1,13 +1,14 @@ import { join } from 'path' -import { typeNamesFromFlowFile } from '../../introspection/flow-ast' +import { buildTypesMap } from '../../introspection' const relative = (p: string) => join(__dirname, p) +const language = 'flow' describe('flow file introspection', () => { test('find all types in file', () => { - const typesNames = typeNamesFromFlowFile({ - path: relative('./mocks/flow-types.js'), - }) + const typesNames = Object.keys( + buildTypesMap(relative('./mocks/flow-types.js'), language), + ) expect(typesNames).toEqual([ 'Interface', @@ -16,19 +17,4 @@ describe('flow file introspection', () => { 'ExportedType', ]) }) - - // test('extract fields from flow type', () => { - // const model: Model = { - // definition: 'Interface', - // absoluteFilePath: relative('./mocks/flow-types.js'), - // importPathRelativeToOutput: 'not_used' - // } - // const fields = extractFieldsFromFlowType(model) - - // expect(fields).toEqual([ - // { fieldName: 'field', fieldOptional: false }, - // { fieldName: 'optionalField', fieldOptional: true }, - // { fieldName: 'fieldUnionNull', fieldOptional: true }, - // ]) - // }) }) diff --git a/packages/graphqlgen/src/tests/typescript/__snapshots__/basic.test.ts.snap b/packages/graphqlgen/src/tests/typescript/__snapshots__/basic.test.ts.snap index 65ce5335..ff255e5b 100644 --- a/packages/graphqlgen/src/tests/typescript/__snapshots__/basic.test.ts.snap +++ b/packages/graphqlgen/src/tests/typescript/__snapshots__/basic.test.ts.snap @@ -5,7 +5,6 @@ exports[`basic enum 1`] = ` import { GraphQLResolveInfo } from \\"graphql\\"; import { User } from \\"../../../fixtures/enum/types\\"; - type Context = any; type EnumAnnotation = \\"EDITOR\\" | \\"COLLABORATOR\\"; @@ -167,7 +166,6 @@ exports[`basic input 1`] = ` import { GraphQLResolveInfo } from \\"graphql\\"; import { AddMemberPayload } from \\"../../../fixtures/input/types\\"; - type Context = any; export namespace MutationResolvers { @@ -333,7 +331,6 @@ exports[`basic scalar 1`] = ` import { GraphQLResolveInfo } from \\"graphql\\"; import { AddMemberPayload } from \\"../../../fixtures/scalar/types\\"; - type Context = any; export namespace MutationResolvers { @@ -450,7 +447,6 @@ exports[`basic schema 1`] = ` import { GraphQLResolveInfo } from \\"graphql\\"; import { Number } from \\"../../../fixtures/basic\\"; - type Context = any; export namespace QueryResolvers { @@ -777,7 +773,6 @@ import { GraphQLResolveInfo } from \\"graphql\\"; import { User } from \\"../../../fixtures/union/types\\"; import { Student } from \\"../../../fixtures/union/types\\"; import { Professor } from \\"../../../fixtures/union/types\\"; - type Context = any; export namespace UserResolvers { @@ -955,7 +950,6 @@ exports[`context 1`] = ` import { GraphQLResolveInfo } from \\"graphql\\"; import { User } from \\"../../../fixtures/context/types\\"; - import { Context } from \\"../../../fixtures/context/types\\"; export namespace QueryResolvers { @@ -1061,7 +1055,6 @@ exports[`defaultName 1`] = ` import { GraphQLResolveInfo } from \\"graphql\\"; import { NumberNode } from \\"../../../fixtures/defaultName\\"; - type Context = any; export namespace QueryResolvers { diff --git a/packages/graphqlgen/src/tests/typescript/__snapshots__/large-schema.test.ts.snap b/packages/graphqlgen/src/tests/typescript/__snapshots__/large-schema.test.ts.snap index 86073853..38a75b9f 100644 --- a/packages/graphqlgen/src/tests/typescript/__snapshots__/large-schema.test.ts.snap +++ b/packages/graphqlgen/src/tests/typescript/__snapshots__/large-schema.test.ts.snap @@ -32,7 +32,6 @@ import { Notification } from \\"../../../fixtures/prisma/types\\"; import { Message } from \\"../../../fixtures/prisma/types\\"; import { AuthPayload } from \\"../../../fixtures/prisma/types\\"; import { MutationResult } from \\"../../../fixtures/prisma/types\\"; - type Context = any; type PLACE_SIZES = From 11ed8ca76f6f649a1df2a31a87f7667c7c26994e Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Mon, 5 Nov 2018 22:19:19 +0100 Subject: [PATCH 15/20] Review fix --- packages/graphqlgen/src/generators/flow-generator.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/graphqlgen/src/generators/flow-generator.ts b/packages/graphqlgen/src/generators/flow-generator.ts index 586e5271..7bf67f8d 100644 --- a/packages/graphqlgen/src/generators/flow-generator.ts +++ b/packages/graphqlgen/src/generators/flow-generator.ts @@ -92,7 +92,6 @@ function renderHeader(args: GenerateArgs): string { import type { GraphQLResolveInfo } from 'graphql' ${modelImports} - ${renderContext(args.context)} ` } From 93d28074c1c2fa448472a9b2ab38c12c974c8c93 Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Mon, 5 Nov 2018 22:20:10 +0100 Subject: [PATCH 16/20] Updated tests --- .../src/tests/flow/__snapshots__/basic.test.ts.snap | 6 ------ .../src/tests/flow/__snapshots__/large-schema.test.ts.snap | 1 - 2 files changed, 7 deletions(-) diff --git a/packages/graphqlgen/src/tests/flow/__snapshots__/basic.test.ts.snap b/packages/graphqlgen/src/tests/flow/__snapshots__/basic.test.ts.snap index 3563eb40..9f977e95 100644 --- a/packages/graphqlgen/src/tests/flow/__snapshots__/basic.test.ts.snap +++ b/packages/graphqlgen/src/tests/flow/__snapshots__/basic.test.ts.snap @@ -6,7 +6,6 @@ exports[`basic enum 1`] = ` import type { GraphQLResolveInfo } from \\"graphql\\"; import type { User } from \\"../../../fixtures/enum/types-flow\\"; - type Context = any; type EnumAnnotation = \\"EDITOR\\" | \\"COLLABORATOR\\"; @@ -166,7 +165,6 @@ exports[`basic scalar 1`] = ` import type { GraphQLResolveInfo } from \\"graphql\\"; import type { AddMemberPayload } from \\"../../../fixtures/scalar/flow-types\\"; - type Context = any; // Types for Mutation @@ -279,7 +277,6 @@ exports[`basic schema 1`] = ` import type { GraphQLResolveInfo } from \\"graphql\\"; import type { Number } from \\"../../../fixtures/basic/types-flow\\"; - type Context = any; // Types for Query @@ -620,7 +617,6 @@ import type { GraphQLResolveInfo } from \\"graphql\\"; import type { User } from \\"../../../fixtures/union/flow-types\\"; import type { Student } from \\"../../../fixtures/union/flow-types\\"; import type { Professor } from \\"../../../fixtures/union/flow-types\\"; - type Context = any; // Types for User @@ -794,7 +790,6 @@ exports[`context 1`] = ` import type { GraphQLResolveInfo } from \\"graphql\\"; import type { User } from \\"../../../fixtures/context/flow-types\\"; - import type { Context } from \\"../../../fixtures/context/flow-types\\"; // Types for Query @@ -898,7 +893,6 @@ exports[`defaultName 1`] = ` import type { GraphQLResolveInfo } from \\"graphql\\"; import type { NumberNode } from \\"../../../fixtures/defaultName/flow-types\\"; - type Context = any; // Types for Query diff --git a/packages/graphqlgen/src/tests/flow/__snapshots__/large-schema.test.ts.snap b/packages/graphqlgen/src/tests/flow/__snapshots__/large-schema.test.ts.snap index 0e0a6f52..4d49711e 100644 --- a/packages/graphqlgen/src/tests/flow/__snapshots__/large-schema.test.ts.snap +++ b/packages/graphqlgen/src/tests/flow/__snapshots__/large-schema.test.ts.snap @@ -33,7 +33,6 @@ import type { Notification } from \\"../../../fixtures/prisma/flow-types\\"; import type { Message } from \\"../../../fixtures/prisma/flow-types\\"; import type { AuthPayload } from \\"../../../fixtures/prisma/flow-types\\"; import type { MutationResult } from \\"../../../fixtures/prisma/flow-types\\"; - type Context = any; type PLACE_SIZES = From b4f53ac314ec1bc37a830d3a741e27ed53b512b5 Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Mon, 5 Nov 2018 22:20:45 +0100 Subject: [PATCH 17/20] Updated templates --- .../flow-yoga/src/generated/graphqlgen.js | 2 +- .../flow-yoga/src/generated/tmp-resolvers/index.js | 5 ++++- .../typescript-yoga/src/generated/graphqlgen.ts | 1 - 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/graphqlgen-templates/flow-yoga/src/generated/graphqlgen.js b/packages/graphqlgen-templates/flow-yoga/src/generated/graphqlgen.js index 3c1b925c..dd07c243 100644 --- a/packages/graphqlgen-templates/flow-yoga/src/generated/graphqlgen.js +++ b/packages/graphqlgen-templates/flow-yoga/src/generated/graphqlgen.js @@ -2,9 +2,9 @@ // Code generated by github.com/prisma/graphqlgen, DO NOT EDIT. import type { GraphQLResolveInfo } from 'graphql' -import type { Context } from '../types' import type { Post } from '../types' import type { User } from '../types' +import type { Context } from '../types' // Types for Query export const Query_defaultResolvers = {} diff --git a/packages/graphqlgen-templates/flow-yoga/src/generated/tmp-resolvers/index.js b/packages/graphqlgen-templates/flow-yoga/src/generated/tmp-resolvers/index.js index e18554de..73a6f457 100644 --- a/packages/graphqlgen-templates/flow-yoga/src/generated/tmp-resolvers/index.js +++ b/packages/graphqlgen-templates/flow-yoga/src/generated/tmp-resolvers/index.js @@ -1,4 +1,7 @@ -/* @flow */ +// @flow +// This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. +// Please do not import this file directly but copy & paste to your application code. + import type { Resolvers } from '../graphqlgen' import { Query } from './Query' diff --git a/packages/graphqlgen-templates/typescript-yoga/src/generated/graphqlgen.ts b/packages/graphqlgen-templates/typescript-yoga/src/generated/graphqlgen.ts index 8e7b2da3..adaceb7e 100644 --- a/packages/graphqlgen-templates/typescript-yoga/src/generated/graphqlgen.ts +++ b/packages/graphqlgen-templates/typescript-yoga/src/generated/graphqlgen.ts @@ -3,7 +3,6 @@ import { GraphQLResolveInfo } from 'graphql' import { Post } from '../types' import { User } from '../types' - import { Context } from '../types' export namespace QueryResolvers { From 35b236db7da7b0f4a429267ad35fad11de89b6cc Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Mon, 5 Nov 2018 22:30:10 +0100 Subject: [PATCH 18/20] Handle interface's field name as string --- packages/graphqlgen/src/introspection/ts-ast.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/graphqlgen/src/introspection/ts-ast.ts b/packages/graphqlgen/src/introspection/ts-ast.ts index da092dd4..d1c63c25 100644 --- a/packages/graphqlgen/src/introspection/ts-ast.ts +++ b/packages/graphqlgen/src/introspection/ts-ast.ts @@ -26,6 +26,7 @@ import { isTSTypeAliasDeclaration, TSEnumDeclaration, isTSEnumDeclaration, + StringLiteral, } from '@babel/types' import { InterfaceDefinition, @@ -171,7 +172,9 @@ function isSupportedTypeOfField(field: TSTypeElement) { function extractInterfaceFieldName(field: TSTypeElement): string { if (isTSPropertySignature(field)) { - return (field.key as Identifier).name + return field.key.type === 'Identifier' + ? (field.key as Identifier).name + : (field.key as StringLiteral).value } return '' From f0493ba84e8e40d7533b76a9540fda94806fb2fb Mon Sep 17 00:00:00 2001 From: schickling Date: Mon, 5 Nov 2018 14:19:17 -0800 Subject: [PATCH 19/20] Released 0.3.0-beta3 --- packages/graphqlgen/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graphqlgen/package.json b/packages/graphqlgen/package.json index e873e515..ce506ba0 100644 --- a/packages/graphqlgen/package.json +++ b/packages/graphqlgen/package.json @@ -1,6 +1,6 @@ { "name": "graphqlgen", - "version": "0.3.0-beta1", + "version": "0.3.0-beta3", "description": "Generate resolver types based on a GraphQL Schema", "main": "dist/index.js", "files": [ From dc12246fd5df1dc8cf670957d2c02349ccadb124 Mon Sep 17 00:00:00 2001 From: schickling Date: Mon, 5 Nov 2018 15:47:34 -0800 Subject: [PATCH 20/20] Released 0.3.0-beta4 --- packages/graphqlgen/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/graphqlgen/package.json b/packages/graphqlgen/package.json index ce506ba0..71ccc7ba 100644 --- a/packages/graphqlgen/package.json +++ b/packages/graphqlgen/package.json @@ -1,6 +1,6 @@ { "name": "graphqlgen", - "version": "0.3.0-beta3", + "version": "0.3.0-beta4", "description": "Generate resolver types based on a GraphQL Schema", "main": "dist/index.js", "files": [ @@ -33,6 +33,7 @@ "homepage": "https://github.com/prisma/graphqlgen#readme", "dependencies": { "@babel/parser": "^7.1.3", + "@babel/types": "7.1.3", "ajv": "^6.5.5", "camelcase": "5.0.0", "chalk": "2.4.1", @@ -49,7 +50,6 @@ "yargs": "12.0.2" }, "devDependencies": { - "@babel/types": "7.1.3", "@types/camelcase": "4.1.0", "@types/graphql": "14.0.3", "@types/jest": "23.3.9",