UNPKG

apollo-codegen-core

Version:
576 lines (510 loc) 15.5 kB
import { print, typeFromAST, getNamedType, isAbstractType, Kind, isCompositeType, GraphQLOutputType, GraphQLInputType, GraphQLObjectType, GraphQLError, GraphQLSchema, GraphQLType, GraphQLCompositeType, GraphQLUnionType, GraphQLInterfaceType, DocumentNode, OperationDefinitionNode, FragmentDefinitionNode, SelectionSetNode, SelectionNode, isSpecifiedScalarType, NonNullTypeNode, GraphQLNonNull, isEnumType, isInputObjectType, isScalarType, isUnionType, isInterfaceType, TypeNode, parseType, } from "graphql"; import { getOperationRootType, getFieldDef, valueFromValueNode, filePathForNode, isMetaFieldName, } from "../utilities/graphql"; import { withTypenameFieldAddedWhereNeeded } from "apollo-language-server"; export interface CompilerOptions { addTypename?: boolean; mergeInFieldsFromFragmentSpreads?: boolean; passthroughCustomScalars?: boolean; customScalarsPrefix?: string; namespace?: string; generateOperationIds?: boolean; operationIdsPath?: string; // this option is only implemented in the ts codegen, so we name it // `ts` fileExtension for now. tsFileExtension?: string; useReadOnlyTypes?: boolean; suppressSwiftMultilineStringLiterals?: boolean; omitDeprecatedEnumCases?: boolean; exposeTypeNodes?: boolean; } export interface CompilerContext { schema: GraphQLSchema; typesUsed: GraphQLType[]; operations: { [operationName: string]: Operation }; fragments: { [fragmentName: string]: Fragment }; options: CompilerOptions; unionTypes: GraphQLUnionType[]; interfaceTypes: Map< GraphQLInterfaceType, (GraphQLObjectType | GraphQLInterfaceType)[] >; } export interface Operation { operationId?: string; operationName: string; operationType: string; variables: { name: string; type: GraphQLType; typeNode?: TypeNode; }[]; filePath: string; source: string; sourceWithFragments?: string; rootType: GraphQLObjectType; selectionSet: SelectionSet; } export interface Fragment { filePath: string; fragmentName: string; source: string; type: GraphQLCompositeType; typeNode?: TypeNode; selectionSet: SelectionSet; } export interface SelectionSet { possibleTypes: GraphQLObjectType[]; selections: Selection[]; } export interface Argument { name: string; value: any; type?: GraphQLInputType; typeNode?: TypeNode; } export type Selection = | Field | TypeCondition | BooleanCondition | FragmentSpread; export interface Field { kind: "Field"; responseKey: string; name: string; alias?: string; args?: Argument[]; type: GraphQLOutputType; typeNode?: TypeNode; description?: string; isDeprecated?: boolean; deprecationReason?: string; isConditional?: boolean; selectionSet?: SelectionSet; } export interface TypeCondition { kind: "TypeCondition"; type: GraphQLCompositeType; typeNode?: TypeNode; selectionSet: SelectionSet; } export interface BooleanCondition { kind: "BooleanCondition"; variableName: string; inverted: boolean; selectionSet: SelectionSet; } export interface FragmentSpread { kind: "FragmentSpread"; fragmentName: string; isConditional?: boolean; selectionSet: SelectionSet; } export function stripProp(propName: string, obj: Object) { let cloned = JSON.parse(JSON.stringify(obj)); for (let prop in cloned) { if (prop === propName) delete cloned[prop]; else if (typeof cloned[prop] === "object") { cloned[prop] = stripProp(propName, cloned[prop]); } } return cloned; } export function compileToIR( schema: GraphQLSchema, document: DocumentNode, options: CompilerOptions = { exposeTypeNodes: true, } ): CompilerContext { if (options.addTypename) { document = withTypenameFieldAddedWhereNeeded(document); } const compiler = new Compiler(schema, options); const operations: { [operationName: string]: Operation } = Object.create(null); const fragments: { [fragmentName: string]: Fragment } = Object.create(null); for (const definition of document.definitions) { switch (definition.kind) { case Kind.OPERATION_DEFINITION: const operation = compiler.compileOperation(definition); operations[operation.operationName] = operation; break; case Kind.FRAGMENT_DEFINITION: const fragment = compiler.compileFragment(definition); fragments[fragment.fragmentName] = fragment; break; } } for (const fragmentSpread of compiler.unresolvedFragmentSpreads) { const fragment = fragments[fragmentSpread.fragmentName]; if (!fragment) { throw new Error(`Cannot find fragment "${fragmentSpread.fragmentName}"`); } // Compute the intersection between the possiblew types of the fragment spread and the fragment. const possibleTypes = fragment.selectionSet.possibleTypes.filter((type) => fragmentSpread.selectionSet.possibleTypes.includes(type) ); fragmentSpread.isConditional = fragment.selectionSet.possibleTypes.some( (type) => !fragmentSpread.selectionSet.possibleTypes.includes(type) ); fragmentSpread.selectionSet = { possibleTypes, selections: fragment.selectionSet.selections, }; } const typesUsed = compiler.typesUsed; const unionTypes = compiler.unionTypes; const interfaceTypes = compiler.interfaceTypes; return { schema, typesUsed, operations, fragments, options, unionTypes, interfaceTypes, }; } class Compiler { options: CompilerOptions; schema: GraphQLSchema; typesUsedSet: Set<GraphQLType>; unionTypesSet: Set<GraphQLUnionType>; interfaceTypesMap: Map< GraphQLInterfaceType, (GraphQLObjectType | GraphQLInterfaceType)[] >; unresolvedFragmentSpreads: FragmentSpread[] = []; constructor(schema: GraphQLSchema, options: CompilerOptions) { this.schema = schema; this.options = options; this.typesUsedSet = new Set(); this.unionTypesSet = new Set(); this.interfaceTypesMap = new Map(); } addTypeUsed(type: GraphQLType) { if (this.typesUsedSet.has(type)) return; if ( isEnumType(type) || isInputObjectType(type) || (isScalarType(type) && !isSpecifiedScalarType(type)) ) { this.typesUsedSet.add(type); } if (isInputObjectType(type)) { for (const field of Object.values(type.getFields())) { this.addTypeUsed(getNamedType(field.type)); } } } get typesUsed(): GraphQLType[] { return Array.from(this.typesUsedSet); } addUnionType(type: GraphQLType) { if (isUnionType(type)) { if (this.unionTypesSet.has(type)) return; this.unionTypesSet.add(type); } } get unionTypes(): GraphQLUnionType[] { return Array.from(this.unionTypesSet); } addInterfaceType(type: GraphQLType) { if (isInterfaceType(type)) { if (this.interfaceTypesMap.has(type)) return; this.interfaceTypesMap.set(type, this.possibleTypesForType(type)); } } get interfaceTypes(): Map< GraphQLInterfaceType, (GraphQLObjectType | GraphQLInterfaceType)[] > { return this.interfaceTypesMap; } compileOperation(operationDefinition: OperationDefinitionNode): Operation { if (!operationDefinition.name) { throw new Error("Operations should be named"); } const filePath = filePathForNode(operationDefinition); const operationName = operationDefinition.name.value; const operationType = operationDefinition.operation; const variables = (operationDefinition.variableDefinitions || []).map( (node) => { const name = node.variable.name.value; const type = typeFromAST(this.schema, node.type as NonNullTypeNode); this.addTypeUsed(getNamedType(type as GraphQLType)); const typeNode = this.options.exposeTypeNodes && type ? stripProp("loc", parseType(type.toString())) : undefined; return { name, type: type as GraphQLNonNull<any>, typeNode, }; } ); const source = print(operationDefinition); const rootType = getOperationRootType( this.schema, operationDefinition ) as GraphQLObjectType; return { filePath, operationName, operationType, variables, source, rootType, selectionSet: this.compileSelectionSet( operationDefinition.selectionSet, rootType ), }; } compileFragment(fragmentDefinition: FragmentDefinitionNode): Fragment { const fragmentName = fragmentDefinition.name.value; const filePath = filePathForNode(fragmentDefinition); const source = print(fragmentDefinition); const type = typeFromAST( this.schema, fragmentDefinition.typeCondition ) as GraphQLCompositeType; const typeNode = this.options.exposeTypeNodes ? stripProp("loc", parseType(type.toString())) : undefined; return { fragmentName, filePath, source, type, selectionSet: this.compileSelectionSet( fragmentDefinition.selectionSet, type ), typeNode, }; } compileSelectionSet( selectionSetNode: SelectionSetNode, parentType: GraphQLCompositeType, possibleTypes: GraphQLObjectType[] = this.possibleTypesForType(parentType), visitedFragments: Set<string> = new Set() ): SelectionSet { return { possibleTypes, selections: selectionSetNode.selections .map((selectionNode) => wrapInBooleanConditionsIfNeeded( this.compileSelection( selectionNode, parentType, possibleTypes, visitedFragments ), selectionNode, possibleTypes ) ) .filter((x) => x) as Selection[], }; } compileSelection( selectionNode: SelectionNode, parentType: GraphQLCompositeType, possibleTypes: GraphQLObjectType[], visitedFragments: Set<string> ): Selection | null { switch (selectionNode.kind) { case Kind.FIELD: { const name = selectionNode.name.value; const alias = selectionNode.alias ? selectionNode.alias.value : undefined; const fieldDef = getFieldDef(this.schema, parentType, selectionNode); if (!fieldDef) { throw new GraphQLError( `Cannot query field "${name}" on type "${String(parentType)}"`, [selectionNode] ); } const fieldType = fieldDef.type; const typeNode = this.options.exposeTypeNodes ? stripProp("loc", parseType(fieldType.toString())) : undefined; const unmodifiedFieldType = getNamedType(fieldType); this.addTypeUsed(unmodifiedFieldType); this.addUnionType(unmodifiedFieldType); this.addInterfaceType(unmodifiedFieldType); const { description, isDeprecated, deprecationReason } = fieldDef; const responseKey = alias || name; const args = selectionNode.arguments && selectionNode.arguments.length > 0 ? selectionNode.arguments.map((arg) => { const name = arg.name.value; const argDef = fieldDef.args.find( (argDef) => argDef.name === arg.name.value ); const argDefType = (argDef && argDef.type) || undefined; const argDeftypeNode = this.options.exposeTypeNodes && argDefType ? stripProp("loc", parseType(argDefType.toString())) : undefined; return { name, value: valueFromValueNode(arg.value), type: argDefType, typeNode: argDeftypeNode, }; }) : undefined; let field: Field = { kind: "Field", responseKey, name, alias, args, type: fieldType, typeNode, description: !isMetaFieldName(name) && description ? description : undefined, isDeprecated, deprecationReason: deprecationReason || undefined, }; if (isCompositeType(unmodifiedFieldType)) { const selectionSetNode = selectionNode.selectionSet; if (!selectionSetNode) { throw new GraphQLError( `Composite field "${name}" on type "${String( parentType )}" requires selection set`, [selectionNode] ); } field.selectionSet = this.compileSelectionSet( selectionNode.selectionSet as SelectionSetNode, unmodifiedFieldType ); } return field; } case Kind.INLINE_FRAGMENT: { const typeNode = selectionNode.typeCondition; const type = typeNode ? (typeFromAST(this.schema, typeNode) as GraphQLCompositeType) : parentType; const possibleTypesForTypeCondition = this.possibleTypesForType( type ).filter((type) => possibleTypes.includes(type)); const typeConditiontypeNode = this.options.exposeTypeNodes ? stripProp("loc", parseType(type.toString())) : undefined; return { kind: "TypeCondition", type, typeNode: typeConditiontypeNode, selectionSet: this.compileSelectionSet( selectionNode.selectionSet, type, possibleTypesForTypeCondition ), }; } case Kind.FRAGMENT_SPREAD: { const fragmentName = selectionNode.name.value; if (visitedFragments.has(fragmentName)) return null; visitedFragments.add(fragmentName); const fragmentSpread: FragmentSpread = { kind: "FragmentSpread", fragmentName, selectionSet: { possibleTypes, selections: [], }, }; this.unresolvedFragmentSpreads.push(fragmentSpread); return fragmentSpread; } } } possibleTypesForType(type: GraphQLCompositeType): GraphQLObjectType[] { if (isAbstractType(type)) { return Array.from(this.schema.getPossibleTypes(type)) || []; } else { return [type]; } } } function wrapInBooleanConditionsIfNeeded( selection: Selection | null, selectionNode: SelectionNode, possibleTypes: GraphQLObjectType[] ): Selection | null { if (!selection) return null; if (!selectionNode.directives) return selection; for (const directive of selectionNode.directives) { const directiveName = directive.name.value; if (directiveName === "skip" || directiveName === "include") { if (!directive.arguments) continue; const value = directive.arguments[0].value; switch (value.kind) { case "BooleanValue": if (directiveName === "skip") { return value.value ? null : selection; } else { return value.value ? selection : null; } break; case "Variable": selection = { kind: "BooleanCondition", variableName: value.name.value, inverted: directiveName === "skip", selectionSet: { possibleTypes, selections: [selection], }, }; break; } } } return selection; }