apollo-codegen-core
Version:
Core generator APIs for Apollo Codegen
576 lines (510 loc) • 15.5 kB
text/typescript
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;
}