apollo-codegen-core
Version:
Core generator APIs for Apollo Codegen
340 lines (306 loc) • 8.96 kB
text/typescript
import {
GraphQLSchema,
GraphQLType,
GraphQLObjectType,
GraphQLCompositeType,
GraphQLInputType,
GraphQLUnionType,
GraphQLInterfaceType,
DocumentNode,
TypeNode,
} from "graphql";
import {
compileToIR,
CompilerContext,
SelectionSet,
Field,
FragmentSpread,
} from "./";
import { collectFragmentsReferenced } from "./visitors/collectFragmentsReferenced";
import { generateOperationId } from "./visitors/generateOperationId";
import { typeCaseForSelectionSet } from "./visitors/typeCase";
import { collectAndMergeFields } from "./visitors/collectAndMergeFields";
import "../utilities/array";
export interface CompilerOptions {
addTypename?: boolean;
mergeInFieldsFromFragmentSpreads?: boolean;
passthroughCustomScalars?: boolean;
customScalarsPrefix?: string;
namespace?: string;
generateOperationIds?: boolean;
exposeTypeNodes?: boolean;
}
export interface LegacyCompilerContext {
schema: GraphQLSchema;
operations: { [operationName: string]: LegacyOperation };
fragments: { [fragmentName: string]: LegacyFragment };
typesUsed: GraphQLType[];
options: CompilerOptions;
unionTypes: GraphQLUnionType[];
interfaceTypes: Map<
GraphQLInterfaceType,
(GraphQLObjectType | GraphQLInterfaceType)[]
>;
}
export interface LegacyOperation {
filePath?: string;
operationName: string;
operationId?: string;
operationType: string;
rootType: GraphQLObjectType;
variables: {
name: string;
type: GraphQLType;
}[];
source: string;
sourceWithFragments?: string;
fields: LegacyField[];
fragmentSpreads?: string[];
inlineFragments?: LegacyInlineFragment[];
fragmentsReferenced: string[];
}
export interface LegacyFragment {
filePath?: string;
fragmentName: string;
source: string;
typeCondition: GraphQLCompositeType;
possibleTypes: GraphQLObjectType[];
fields: LegacyField[];
fragmentSpreads: string[];
inlineFragments: LegacyInlineFragment[];
}
export interface LegacyInlineFragment {
typeCondition: GraphQLObjectType;
possibleTypes: GraphQLObjectType[];
fields: LegacyField[];
fragmentSpreads: string[];
}
export interface LegacyField {
responseName: string;
fieldName: string;
args?: Argument[];
type: GraphQLType;
typeNode?: TypeNode;
description?: string;
isConditional?: boolean;
conditions?: BooleanCondition[];
isDeprecated?: boolean;
deprecationReason?: string;
fields?: LegacyField[];
fragmentSpreads?: string[];
inlineFragments?: LegacyInlineFragment[];
}
export interface BooleanCondition {
variableName: string;
inverted: boolean;
}
export interface Argument {
name: string;
value: any;
type?: GraphQLInputType;
}
export function compileToLegacyIR(
schema: GraphQLSchema,
document: DocumentNode,
options: CompilerOptions = {
mergeInFieldsFromFragmentSpreads: true,
exposeTypeNodes: false,
}
): LegacyCompilerContext {
const context = compileToIR(schema, document, options);
const transformer = new LegacyIRTransformer(context, options);
return transformer.transformIR();
}
class LegacyIRTransformer {
constructor(
public context: CompilerContext,
public options: CompilerOptions = { mergeInFieldsFromFragmentSpreads: true }
) {}
transformIR(): LegacyCompilerContext {
const operations: {
[operationName: string]: LegacyOperation;
} = Object.create({});
for (const [operationName, operation] of Object.entries(
this.context.operations
)) {
const {
filePath,
operationType,
rootType,
variables,
source,
selectionSet,
} = operation;
const fragmentsReferenced = collectFragmentsReferenced(
selectionSet,
this.context.fragments
);
const { sourceWithFragments, operationId } = generateOperationId(
operation,
this.context.fragments,
fragmentsReferenced
);
operations[operationName] = {
filePath,
operationName,
operationType,
rootType,
variables,
source,
...this.transformSelectionSetToLegacyIR(selectionSet),
fragmentsReferenced: Array.from(fragmentsReferenced),
sourceWithFragments,
operationId,
};
}
const fragments: { [fragmentName: string]: LegacyFragment } = Object.create(
{}
);
for (const [fragmentName, fragment] of Object.entries(
this.context.fragments
)) {
const { selectionSet, type, ...fragmentWithoutSelectionSet } = fragment;
fragments[fragmentName] = {
typeCondition: type,
possibleTypes: selectionSet.possibleTypes,
...fragmentWithoutSelectionSet,
...this.transformSelectionSetToLegacyIR(selectionSet),
};
}
const legacyContext: LegacyCompilerContext = {
schema: this.context.schema,
operations,
fragments,
typesUsed: this.context.typesUsed,
options: this.options,
unionTypes: this.context.unionTypes,
interfaceTypes: this.context.interfaceTypes,
};
return legacyContext;
}
transformSelectionSetToLegacyIR(selectionSet: SelectionSet) {
const typeCase = typeCaseForSelectionSet(
selectionSet,
!!this.options.mergeInFieldsFromFragmentSpreads
);
const fields: LegacyField[] = this.transformFieldsToLegacyIR(
collectAndMergeFields(typeCase.default, false)
);
const inlineFragments: LegacyInlineFragment[] = typeCase.variants.flatMap(
(variant) => {
const fields = this.transformFieldsToLegacyIR(
collectAndMergeFields(variant, false)
);
if (
// Filter out records that represent the same possible types as the default record.
selectionSet.possibleTypes.every((type) =>
variant.possibleTypes.includes(type)
) &&
// Filter out empty records for consistency with legacy compiler.
fields.length < 1
)
return undefined;
const fragmentSpreads: string[] = this.collectFragmentSpreads(
selectionSet,
variant.possibleTypes
).map((fragmentSpread: FragmentSpread) => fragmentSpread.fragmentName);
return variant.possibleTypes.map((possibleType) => {
return {
typeCondition: possibleType,
possibleTypes: [possibleType],
fields,
fragmentSpreads,
} as LegacyInlineFragment;
});
}
);
for (const inlineFragment of inlineFragments) {
inlineFragments[inlineFragment.typeCondition.name as any] =
inlineFragment;
}
const fragmentSpreads: string[] = this.collectFragmentSpreads(
selectionSet
).map((fragmentSpread: FragmentSpread) => fragmentSpread.fragmentName);
return {
fields,
fragmentSpreads,
inlineFragments,
};
}
transformFieldsToLegacyIR(fields: Field[]) {
return fields.map((field) => {
const {
args,
type,
typeNode,
isConditional,
description,
isDeprecated,
deprecationReason,
selectionSet,
} = field;
const conditions =
field.conditions && field.conditions.length > 0
? field.conditions.map(({ kind, variableName, inverted }) => {
return {
kind,
variableName,
inverted,
};
})
: undefined;
return {
responseName: field.alias || field.name,
fieldName: field.name,
type,
typeNode,
args,
isConditional,
conditions,
description,
isDeprecated,
deprecationReason,
...(selectionSet
? this.transformSelectionSetToLegacyIR(selectionSet)
: {}),
} as LegacyField;
});
}
collectFragmentSpreads(
selectionSet: SelectionSet,
possibleTypes: GraphQLObjectType[] = selectionSet.possibleTypes
): FragmentSpread[] {
const fragmentSpreads: FragmentSpread[] = [];
for (const selection of selectionSet.selections) {
switch (selection.kind) {
case "FragmentSpread":
fragmentSpreads.push(selection);
break;
case "TypeCondition":
if (
possibleTypes.every((type) =>
selection.selectionSet.possibleTypes.includes(type)
)
) {
fragmentSpreads.push(
...this.collectFragmentSpreads(
selection.selectionSet,
possibleTypes
)
);
}
break;
case "BooleanCondition":
fragmentSpreads.push(
...this.collectFragmentSpreads(
selection.selectionSet,
possibleTypes
)
);
break;
}
}
// Unique the fragment spreads before returning them.
return Array.from(new Set(fragmentSpreads));
}
}