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 1609abeb..adaceb7e 100644 --- a/packages/graphqlgen-templates/typescript-yoga/src/generated/graphqlgen.ts +++ b/packages/graphqlgen-templates/typescript-yoga/src/generated/graphqlgen.ts @@ -1,9 +1,9 @@ // 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 +141,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/package.json b/packages/graphqlgen/package.json index 09567543..9228abad 100644 --- a/packages/graphqlgen/package.json +++ b/packages/graphqlgen/package.json @@ -1,6 +1,6 @@ { "name": "graphqlgen", - "version": "0.2.15", + "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.5", "@types/camelcase": "4.1.0", "@types/graphql": "14.0.3", "@types/jest": "23.3.9", diff --git a/packages/graphqlgen/src/generators/common.ts b/packages/graphqlgen/src/generators/common.ts index f9a5ab92..d959b797 100644 --- a/packages/graphqlgen/src/generators/common.ts +++ b/packages/graphqlgen/src/generators/common.ts @@ -4,10 +4,21 @@ import { GraphQLTypeObject, GraphQLType, GraphQLTypeField, + getGraphQLEnumValues, } from '../source-helper' -import { Model, ModelMap, ContextDefinition } from '../types' -import { ModelField } from '../introspection/ts-ast' +import { ModelMap, ContextDefinition, GenerateArgs } from '../types' import { flatten, uniq } from '../utils' +import { + TypeDefinition, + FieldDefinition, + InterfaceDefinition, + TypeAliasDefinition, + AnonymousInterfaceAnnotation, +} from '../introspection/types' +import { + isFieldDefinitionEnumOrLiteral, + getEnumValues, +} from '../introspection/utils' type SpecificGraphQLScalarType = 'boolean' | 'number' | 'string' @@ -19,28 +30,56 @@ export interface TypeToInputTypeAssociation { [objectTypeName: string]: string[] } +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 + } + // If model is of type `type TypeName = { ... }` + if ( + modelDef.kind === 'TypeAliasDefinition' && + (modelDef as TypeAliasDefinition).getType().kind === + 'AnonymousInterfaceAnnotation' + ) { + const interfaceDef = (modelDef as TypeAliasDefinition).getType() as AnonymousInterfaceAnnotation + + return interfaceDef.fields + } + + return [] +} + export function renderDefaultResolvers( - type: GraphQLTypeObject, - modelMap: ModelMap, - extractFieldsFromModel: (model: Model) => ModelField[], + graphQLTypeObject: GraphQLTypeObject, + args: GenerateArgs, variableName: string, ): string { - const model = modelMap[type.name] + const model = args.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 => { + const graphQLField = graphQLTypeObject.fields.find( + field => field.name === modelField.name, + ) + + return shouldRenderDefaultResolver(graphQLField, modelField, args) + }) .map(modelField => renderDefaultResolver( - modelField.fieldName, - modelField.fieldOptional, - model.modelTypeName, + modelField.name, + modelField.optional, + model.definition.name, ), ) .join(os.EOL)} @@ -90,37 +129,60 @@ export function getModelName(type: GraphQLType, modelMap: ModelMap): string { return '{}' } - return model.modelTypeName + return model.definition.name } -function shouldRenderDefaultResolver( - type: GraphQLTypeObject, - modelField: ModelField, +function isModelEnumSubsetOfGraphQLEnum( + graphQLEnumValues: string[], + modelEnumValues: string[], ) { - const graphQLField = type.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( @@ -133,7 +195,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 +251,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..7bf67f8d 100644 --- a/packages/graphqlgen/src/generators/flow-generator.ts +++ b/packages/graphqlgen/src/generators/flow-generator.ts @@ -3,16 +3,16 @@ 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, getContextName, + getDistinctInputTypes, getModelName, - TypeToInputTypeAssociation, InputTypesMap, printFieldLikeType, - getDistinctInputTypes, + renderDefaultResolvers, + renderEnums, + TypeToInputTypeAssociation, } from './common' export function format(code: string, options: prettier.Options = {}) { @@ -67,6 +67,8 @@ export function generate(args: GenerateArgs): string { return `\ ${renderHeader(args)} + ${renderEnums(args)} + ${renderNamespaces(args, typeToInputTypeAssociation, inputTypesMap)} ${renderResolvers(args)} @@ -79,7 +81,7 @@ function renderHeader(args: GenerateArgs): string { const modelImports = modelArray .map( m => - `import type { ${m.modelTypeName} } from '${ + `import type { ${m.definition.name} } from '${ m.importPathRelativeToOutput }'`, ) @@ -90,7 +92,6 @@ function renderHeader(args: GenerateArgs): string { import type { GraphQLResolveInfo } from 'graphql' ${modelImports} - ${renderContext(args.context)} ` } @@ -113,13 +114,7 @@ function renderNamespaces( return args.types .filter(type => type.type.isObject) .map(type => - renderNamespace( - type, - typeToInputTypeAssociation, - inputTypesMap, - args.modelMap, - args.context, - ), + renderNamespace(type, typeToInputTypeAssociation, inputTypesMap, args), ) .join(os.EOL) } @@ -128,32 +123,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, - extractFieldsFromFlowType, - `${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) */ ''} ` @@ -171,7 +160,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)}`, )} diff --git a/packages/graphqlgen/src/generators/flow-scaffolder.ts b/packages/graphqlgen/src/generators/flow-scaffolder.ts index f6931db8..190ddce7 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' @@ -31,12 +33,35 @@ 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, - 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 +71,7 @@ export const ${type.name}: ${upperTypeName}_Resolvers = { ...${upperTypeName}_defaultResolvers, ${type.fields .filter(graphQLField => - shouldScaffoldFieldResolver(graphQLField, modelFields), + shouldScaffoldFieldResolver(graphQLField, modelFields, args), ) .map( field => ` @@ -68,7 +93,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 @@ -79,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 diff --git a/packages/graphqlgen/src/generators/ts-generator.ts b/packages/graphqlgen/src/generators/ts-generator.ts index 4f512afc..a0dffdef 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 { extractFieldsFromTypescriptType } from '../introspection/ts-ast' -import { upperFirst } from '../utils' import { renderDefaultResolvers, getContextName, @@ -13,7 +11,10 @@ import { InputTypesMap, printFieldLikeType, getDistinctInputTypes, + renderEnums, } from './common' +import { TypeAliasDefinition } from '../introspection/types' +import { upperFirst } from '../utils' export function format(code: string, options: prettier.Options = {}) { try { @@ -66,6 +67,8 @@ export function generate(args: GenerateArgs): string { return `\ ${renderHeader(args)} + + ${renderEnums(args)} ${renderNamespaces(args, typeToInputTypeAssociation, inputTypesMap)} @@ -75,11 +78,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) @@ -88,7 +103,6 @@ function renderHeader(args: GenerateArgs): string { import { GraphQLResolveInfo } from 'graphql' ${modelImports} - ${renderContext(args.context)} ` } @@ -109,46 +123,42 @@ function renderNamespaces( return args.types .filter(type => type.type.isObject) .map(type => - renderNamespace( - type, - typeToInputTypeAssociation, - inputTypesMap, - args.modelMap, - args.context, - ), + renderNamespace(type, typeToInputTypeAssociation, inputTypesMap, args), ) .join(os.EOL) } function renderNamespace( - type: GraphQLTypeObject, + graphQLTypeObject: GraphQLTypeObject, typeToInputTypeAssociation: TypeToInputTypeAssociation, inputTypesMap: InputTypesMap, - modelMap: ModelMap, - context?: ContextDefinition, + args: GenerateArgs, ): string { return `\ - export namespace ${type.name}Resolvers { + export namespace ${graphQLTypeObject.name}Resolvers { - ${renderDefaultResolvers( - type, - modelMap, - extractFieldsFromTypescriptType, - 'defaultResolvers', - )} + ${renderDefaultResolvers(graphQLTypeObject, args, 'defaultResolvers')} ${renderInputTypeInterfaces( - type, - modelMap, + graphQLTypeObject, + args.modelMap, typeToInputTypeAssociation, inputTypesMap, )} - ${renderInputArgInterfaces(type, modelMap)} + ${renderInputArgInterfaces(graphQLTypeObject, args.modelMap)} - ${renderResolverFunctionInterfaces(type, modelMap, context)} + ${renderResolverFunctionInterfaces( + graphQLTypeObject, + args.modelMap, + args.context, + )} - ${renderResolverTypeInterface(type, modelMap, context)} + ${renderResolverTypeInterface( + graphQLTypeObject, + args.modelMap, + args.context, + )} ${/* TODO renderResolverClass(type, modelMap) */ ''} } @@ -169,8 +179,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/generators/ts-scaffolder.ts b/packages/graphqlgen/src/generators/ts-scaffolder.ts index 572101c2..880976ee 100644 --- a/packages/graphqlgen/src/generators/ts-scaffolder.ts +++ b/packages/graphqlgen/src/generators/ts-scaffolder.ts @@ -1,7 +1,9 @@ -import { GenerateArgs, CodeFileLike, ModelMap } from '../types' +import { GenerateArgs, CodeFileLike } 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' @@ -45,10 +47,10 @@ function isParentType(name: string) { function renderResolvers( type: GraphQLTypeObject, - modelMap: ModelMap, + args: GenerateArgs, ): CodeFileLike { - const model = modelMap[type.name] - const modelFields = extractFieldsFromTypescriptType(model) + const model = args.modelMap[type.name] + const modelFields = fieldsFromModelDefinition(model.definition) const code = `\ // This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. @@ -59,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' : ''}) => { @@ -122,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/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/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 2a5221f4..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, @@ -7,31 +5,52 @@ 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' -//TODO: Add caching with { [filePath: string]: ExtractableType[] } or something 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' } -function getFlowTypes(sourceFile: FlowFile): ExtractableType[] { +function findFlowTypes(sourceFile: FlowFile): ExtractableType[] { const statements = sourceFile.program.body const types = statements.filter(shouldExtractType) @@ -50,23 +69,7 @@ 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) -} - -function isFieldOptional(node: ObjectTypeProperty) { +export function isFieldOptional(node: ObjectTypeProperty) { if (!!node.optional) { return true } @@ -84,26 +87,159 @@ function isFieldOptional(node: ObjectTypeProperty) { return false } -export function extractFieldsFromFlowType(model: Model): ModelField[] { - const filePath = model.absoluteFilePath - const typeNode = findFlowTypeByName(filePath, model.modelTypeName) +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.modelTypeName}`) + return createTypeReferenceAnnotation(referenceTypeName) } + if (isArrayTypeAnnotation(node)) { + const computedType = computeType(node.elementType, filePath) + + if (computedType.kind !== 'TypeReferenceAnnotation') { + computedType.isArray = true + } - const childrenNodes = - typeNode.type === 'TypeAlias' - ? (typeNode as TypeAlias).right - : (typeNode as InterfaceDeclaration).body + return computedType + } + if ( + isStringLiteralTypeAnnotation(node) || + isNumberLiteralTypeAnnotation(node) || + isBooleanLiteralTypeAnnotation(node) + ) { + const literalValue = node.value + + return createLiteralTypeAnnotation(typeof literalValue, literalValue) + } - 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) + if (isObjectTypeAnnotation(node)) { + const interfaceFields = extractInterfaceFields(node.properties, filePath) - return { fieldName, fieldOptional } - }) + return createAnonymousInterfaceAnnotation(interfaceFields) + } + + if (isUnionTypeAnnotation(node)) { + const unionTypes = node.types.map(unionType => + computeType(unionType, filePath), + ) + + return createUnionTypeAnnotation(unionTypes, filePath) + } + + 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 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, +) { + return fields.map(field => { + const fieldName = extractInterfaceFieldName(field) + + if (!isSupportedTypeOfField(field)) { + return createInterfaceField( + '', + createTypeAnnotation('_UNKNOWN_TYPE_'), + filePath, + false, + ) + } + + const fieldAsObjectTypeProperty = field as ObjectTypeProperty + const fieldType = computeType(fieldAsObjectTypeProperty.value, filePath) + const isOptional = isFieldOptional(fieldAsObjectTypeProperty) + + 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'], + sourceType: 'module', + }) + + 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 d2774425..d1c63c25 100644 --- a/packages/graphqlgen/src/introspection/ts-ast.ts +++ b/packages/graphqlgen/src/introspection/ts-ast.ts @@ -1,46 +1,219 @@ -import * as fs from 'fs' -import { File } from 'graphqlgen-json-schema' -import { getPath } from '../parse' -import { Model } from '../types' import { parse as parseTS } from '@babel/parser' import { + ExportNamedDeclaration, File as TSFile, - TSTypeAliasDeclaration, - TSInterfaceDeclaration, + Identifier, + isTSPropertySignature, + isTSTypeLiteral, Statement, - ExportNamedDeclaration, - TSTypeLiteral, - TSInterfaceBody, + TSInterfaceDeclaration, TSPropertySignature, - Identifier, + TSTypeAliasDeclaration, + TSTypeElement, TSUnionType, + TSType, + isTSParenthesizedType, + isTSStringKeyword, + isTSNumberKeyword, + isTSBooleanKeyword, + isTSAnyKeyword, + isTSNullKeyword, + isTSUndefinedKeyword, + isTSTypeReference, + isTSArrayType, + isTSLiteralType, + isTSUnionType, + isTSTypeAliasDeclaration, + TSEnumDeclaration, + isTSEnumDeclaration, + StringLiteral, } from '@babel/types' +import { + InterfaceDefinition, + TypeAliasDefinition, + TypesMap, + InternalInnerType, +} from './types' +import { + createInterface, + createInterfaceField, + createTypeAlias, + createTypeAnnotation, + createTypeReferenceAnnotation, + createLiteralTypeAnnotation, + createAnonymousInterfaceAnnotation, + createUnionTypeAnnotation, +} from './factory' -export interface InterfaceNamesToFile { - [interfaceName: string]: File -} +// /!\ If you add a supported type of field, make sure you update isSupportedField() as well +type SupportedFields = TSPropertySignature -export interface ModelField { - fieldName: string - fieldOptional: boolean +type ExtractableType = + | TSTypeAliasDeclaration + | TSInterfaceDeclaration + | TSEnumDeclaration + +function shouldExtractType(node: Statement) { + return ( + node.type === 'TSTypeAliasDeclaration' || + node.type === 'TSInterfaceDeclaration' || + node.type === 'TSEnumDeclaration' + ) } -type ExtractableType = TSTypeAliasDeclaration | TSInterfaceDeclaration +export function computeType(node: TSType, filePath: string): InternalInnerType { + 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) || isTSUndefinedKeyword(node)) { + return createTypeAnnotation(null) + } + if (isTSTypeReference(node)) { + const referenceTypeName = (node.typeName as Identifier).name + + return createTypeReferenceAnnotation(referenceTypeName) + } + if (isTSArrayType(node)) { + const computedType = computeType(node.elementType, filePath) + + if (computedType.kind !== 'TypeReferenceAnnotation') { + 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), + ) -function getSourceFile(filePath: string): TSFile { - const file = fs.readFileSync(filePath).toString() + return createUnionTypeAnnotation(unionTypes, filePath) + } - return parseTS(file, { plugins: ['typescript'], sourceType: 'module' }) + return createTypeAnnotation('_UNKNOWN_TYPE_') } -function shouldExtractType(node: Statement) { - return ( - node.type === 'TSTypeAliasDeclaration' || - node.type === 'TSInterfaceDeclaration' +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, filePath) + } +} + +// 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) +} + +function extractInterfaceFieldName(field: TSTypeElement): string { + if (isTSPropertySignature(field)) { + return field.key.type === 'Identifier' + ? (field.key as Identifier).name + : (field.key as StringLiteral).value + } + + return '' +} + +function extractInterfaceFields(fields: TSTypeElement[], filePath: string) { + return fields.map(field => { + const fieldName = extractInterfaceFieldName(field) + + if (!isSupportedTypeOfField(field) || !field.typeAnnotation) { + return createInterfaceField( + '', + createTypeAnnotation('_UNKNOWN_TYPE_'), + filePath, + false, + ) + } + + const fieldType = computeType( + field.typeAnnotation!.typeAnnotation, + filePath, + ) + const isOptional = isFieldOptional(field as TSPropertySignature) + + return createInterfaceField(fieldName, fieldType, filePath, isOptional) + }) +} + +function extractInterface( + typeName: string, + fields: TSTypeElement[], + filePath: string, +): InterfaceDefinition { + const interfaceFields = extractInterfaceFields(fields, filePath) + + return createInterface(typeName, interfaceFields) } -function getTypescriptTypes(sourceFile: TSFile): ExtractableType[] { +function findTypescriptTypes(sourceFile: TSFile): ExtractableType[] { const statements = sourceFile.program.body const types = statements.filter(shouldExtractType) @@ -59,22 +232,6 @@ function getTypescriptTypes(sourceFile: TSFile): ExtractableType[] { return [...types, ...typesFromNamedExport] as ExtractableType[] } -export function findTypescriptInterfaceByName( - filePath: string, - typeName: string, -): ExtractableType | undefined { - const sourceFile = getSourceFile(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) -} - function isFieldOptional(node: TSPropertySignature) { if (!!node.optional) { return true @@ -99,36 +256,37 @@ function isFieldOptional(node: TSPropertySignature) { return false } -export function extractFieldsFromTypescriptType(model: Model): ModelField[] { - const filePath = model.absoluteFilePath - const typeNode = findTypescriptInterfaceByName(filePath, model.modelTypeName) +export function buildTSTypesMap(fileContent: string, filePath: string) { + const ast = parseTS(fileContent, { + plugins: ['typescript'], + sourceType: 'module', + }) - if (!typeNode) { - throw new Error(`No interface found for name ${model.modelTypeName}`) - } + const typesMap = findTypescriptTypes(ast).reduce( + (acc, type) => { + const typeName = type.id.name - if ( - typeNode.type === 'TSTypeAliasDeclaration' && - (typeNode as TSTypeAliasDeclaration).typeAnnotation.type !== 'TSTypeLiteral' - ) { - throw new Error( - `Type notation not supported for type ${model.modelTypeName}`, - ) - } + if (isTSTypeAliasDeclaration(type)) { + return { + ...acc, + [typeName]: extractTypeAlias(typeName, type, filePath), + } + } - const childrenNodes = - typeNode.type === 'TSTypeAliasDeclaration' - ? ((typeNode as TSTypeAliasDeclaration).typeAnnotation as TSTypeLiteral) - .members - : ((typeNode as TSInterfaceDeclaration).body as TSInterfaceBody).body + if (isTSEnumDeclaration(type)) { + return { + ...acc, + [typeName]: extractEnum(typeName, type, filePath), + } + } - 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 { + ...acc, + [typeName]: extractInterface(typeName, type.body.body, filePath), + } + }, + {} as TypesMap, + ) - return { fieldName, fieldOptional } - }) + 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..86c23e65 --- /dev/null +++ b/packages/graphqlgen/src/introspection/utils.ts @@ -0,0 +1,87 @@ +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' + ) + }) +} diff --git a/packages/graphqlgen/src/parse.ts b/packages/graphqlgen/src/parse.ts index fd04bbab..af9372a4 100644 --- a/packages/graphqlgen/src/parse.ts +++ b/packages/graphqlgen/src/parse.ts @@ -12,13 +12,24 @@ 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, } from './path-helpers' import { getTypeToFileMapping, replaceAll, normalizeFilePath } from './utils' -import { extractTypes, extractGraphQLTypesWithoutRootsAndInputs, GraphQLTypes } from './source-helper' +import { + extractTypes, + extractGraphQLTypesWithoutRootsAndInputsAndEnums, + GraphQLTypes, +} from './source-helper' +import { FilesToTypesMap } from './introspection/types' +import { buildFilesToTypesMap } from './introspection' + +export interface NormalizedFile { + path: string + defaultName?: string +} const ajv = new Ajv().addMetaSchema( require('ajv/lib/refs/json-schema-draft-06.json'), @@ -101,11 +112,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, @@ -114,7 +126,7 @@ function buildModel( return { absoluteFilePath, importPathRelativeToOutput, - modelTypeName: modelName, + definition: filesToTypesMap[filePath][modelName], } } @@ -134,21 +146,32 @@ export function getDefaultName(file: File): string | null { return file.defaultName || null } +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, outputDir: string, language: Language, ): ModelMap { - const graphQLTypes = extractGraphQLTypesWithoutRootsAndInputs(schema) - const filePaths = !!models.files - ? models.files.map(file => ({ - defaultName: typeof file === 'object' ? file.defaultName : undefined, - path: normalizeFilePath(getPath(file), language), - })) - : [] + const graphQLTypes = extractGraphQLTypesWithoutRootsAndInputsAndEnums(schema) + const normalizedFiles = normalizeFiles(models.files, language) + const filesToTypesMap = buildFilesToTypesMap(normalizedFiles, language) 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]) { @@ -156,7 +179,13 @@ export function parseModels( return { ...acc, - [type.name]: buildModel(filePath, modelName, outputDir, language), + [type.name]: buildModel( + modelName, + filePath, + filesToTypesMap, + outputDir, + language, + ), } } @@ -188,7 +217,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/source-helper.ts b/packages/graphqlgen/src/source-helper.ts index 60b65325..6f004602 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,12 +346,32 @@ 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, ) } + +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/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/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/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..9f977e95 100644 --- a/packages/graphqlgen/src/tests/flow/__snapshots__/basic.test.ts.snap +++ b/packages/graphqlgen/src/tests/flow/__snapshots__/basic.test.ts.snap @@ -5,17 +5,18 @@ 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; +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 = ( @@ -38,7 +39,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 = ( @@ -55,12 +56,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 -) => UserType | Promise; +) => EnumAnnotation | Promise; + +export type User_EnumAsUnionType_Resolver = ( + parent: User, + args: {}, + ctx: Context, + info: GraphQLResolveInfo +) => EnumAsUnionType | Promise; export interface User_Resolvers { id: ( @@ -77,12 +85,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 { @@ -100,7 +115,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, @@ -120,16 +139,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 }; ", @@ -145,13 +165,12 @@ exports[`basic scalar 1`] = ` import type { GraphQLResolveInfo } from \\"graphql\\"; import type { AddMemberPayload } from \\"../../../fixtures/scalar/flow-types\\"; - type Context = any; // Types for Mutation export const Mutation_defaultResolvers = {}; -export interface AddMemberData { +export interface Mutation_AddMemberData { email: string; projects: string[]; } @@ -232,16 +251,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 }; ", @@ -257,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 @@ -570,7 +589,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\\"; @@ -595,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 @@ -741,7 +762,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\\"; @@ -766,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 @@ -844,7 +867,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\\"; @@ -867,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 @@ -1180,7 +1205,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 093b4a3c..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 @@ -19,27 +19,40 @@ 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\\"; - 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 = {}; @@ -4220,7 +4233,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\\"; @@ -4238,21 +4254,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\\"; @@ -4274,21 +4286,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/flow.test.ts b/packages/graphqlgen/src/tests/introspection/flow.test.ts index 32a45d25..120f6e4d 100644 --- a/packages/graphqlgen/src/tests/introspection/flow.test.ts +++ b/packages/graphqlgen/src/tests/introspection/flow.test.ts @@ -1,28 +1,20 @@ -import { join } from "path"; -import { Model } from "../../types"; -import { typeNamesFromFlowFile, extractFieldsFromFlowType } from "../../introspection/flow-ast"; +import { join } from 'path' +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', '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) - - expect(fields).toEqual([ - { fieldName: 'field', fieldOptional: false }, - { fieldName: 'optionalField', fieldOptional: true }, - { fieldName: 'fieldUnionNull', fieldOptional: true }, + expect(typesNames).toEqual([ + 'Interface', + 'Type', + 'ExportedInterface', + 'ExportedType', ]) }) }) 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 461cb2c5..91e51bcf 100644 --- a/packages/graphqlgen/src/tests/introspection/typescript.test.ts +++ b/packages/graphqlgen/src/tests/introspection/typescript.test.ts @@ -1,31 +1,33 @@ -import { - extractFieldsFromTypescriptType, - typeNamesFromTypescriptFile -} from "../../introspection/ts-ast"; -import { join } from "path"; -import { Model } from "../../types"; +import { join } from 'path' +import { buildTypesMap } from '../../introspection' const relative = (p: string) => join(__dirname, p) +const language = 'typescript' 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'), language), + ) - 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', + 'Enum', + '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..ff255e5b 100644 --- a/packages/graphqlgen/src/tests/typescript/__snapshots__/basic.test.ts.snap +++ b/packages/graphqlgen/src/tests/typescript/__snapshots__/basic.test.ts.snap @@ -4,17 +4,18 @@ 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; +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 = ( @@ -38,7 +39,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 = ( @@ -55,12 +56,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: ( @@ -77,12 +85,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; } } @@ -102,7 +117,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, @@ -147,7 +166,6 @@ exports[`basic input 1`] = ` import { GraphQLResolveInfo } from \\"graphql\\"; import { AddMemberPayload } from \\"../../../fixtures/input/types\\"; - type Context = any; export namespace MutationResolvers { @@ -313,7 +331,6 @@ exports[`basic scalar 1`] = ` import { GraphQLResolveInfo } from \\"graphql\\"; import { AddMemberPayload } from \\"../../../fixtures/scalar/types\\"; - type Context = any; export namespace MutationResolvers { @@ -430,7 +447,6 @@ exports[`basic schema 1`] = ` import { GraphQLResolveInfo } from \\"graphql\\"; import { Number } from \\"../../../fixtures/basic\\"; - type Context = any; export namespace QueryResolvers { @@ -757,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 { @@ -935,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 { @@ -1041,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 3f07a55d..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 @@ -18,27 +18,40 @@ 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\\"; - 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 = {}; diff --git a/packages/graphqlgen/src/types.ts b/packages/graphqlgen/src/types.ts index 97a52253..2168a907 100644 --- a/packages/graphqlgen/src/types.ts +++ b/packages/graphqlgen/src/types.ts @@ -4,6 +4,7 @@ import { GraphQLEnumObject, GraphQLUnionObject, } from './source-helper' +import { TypeDefinition } from './introspection/types' export interface GenerateArgs { types: GraphQLTypeObject[] @@ -20,7 +21,7 @@ export interface ModelMap { export interface Model { absoluteFilePath: string importPathRelativeToOutput: string - modelTypeName: string + definition: TypeDefinition } export interface ContextDefinition { diff --git a/packages/graphqlgen/src/utils.ts b/packages/graphqlgen/src/utils.ts index 0db355a2..43b6ca90 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 { NormalizedFile } from './parse' +import { FilesToTypesMap, InterfaceNamesToFile } from './introspection/types' 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 93c945ba..aee598b4 100644 --- a/packages/graphqlgen/src/validation.ts +++ b/packages/graphqlgen/src/validation.ts @@ -1,12 +1,6 @@ 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 { outputDefinitionFilesNotFound, outputInterfaceDefinitionsNotFound, @@ -15,12 +9,18 @@ import { outputWrongSyntaxFiles, } from './output' import { - extractGraphQLTypesWithoutRootsAndInputs, + extractGraphQLTypesWithoutRootsAndInputsAndEnums, 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, + NormalizedFile, +} from './parse' +import { buildFilesToTypesMap, findTypeInFile } from './introspection' type Definition = { typeName: string @@ -108,16 +108,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 +138,7 @@ export function validateModels( return validateSchemaToModelMapping( schema, validatedOverriddenModels, - filePaths, + normalizedFiles, language, ) } @@ -174,14 +172,19 @@ function testValidatedDefinitions( function validateSchemaToModelMapping( schema: GraphQLTypes, validatedOverriddenModels: ValidatedDefinition[], - files: File[], + normalizedFiles: NormalizedFile[], language: Language, ): boolean { - const graphQLTypes = extractGraphQLTypesWithoutRootsAndInputs(schema) + const graphQLTypes = extractGraphQLTypesWithoutRootsAndInputsAndEnums(schema) const overridenTypeNames = validatedOverriddenModels.map( def => def.definition.typeName, ) - const interfaceNamesToPath = getTypeToFileMapping(files, language) + + const filesToTypesMap = buildFilesToTypesMap(normalizedFiles, language) + 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 +208,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] } @@ -218,26 +221,15 @@ 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 } -// 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 +267,7 @@ export function validateDefinition( return validation } - if ( - !interfaceDefinitionExistsInFile(normalizedFilePath, modelName, language) - ) { + if (!findTypeInFile(normalizedFilePath, modelName, language)) { validation.interfaceExists = false }