@gql2ts/from-query
Version:
generate typescript interfaces from a graphql schema and query
376 lines • 13.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const graphql_1 = require("graphql");
/**
* Given an Array of DirectiveNodes, return a map of directive names to their IR
*
* @param directives The directives provided by the AST
* @returns A map of directives
*/
const extractDirectives = directives => !directives ? {} : directives.reduce((directiveMap, { name: { value: name }, arguments: args = [] }) => (Object.assign({}, directiveMap, { [name]: {
kind: 'Directive',
name,
// @TODO support all of ValueNode
arguments: args.reduce((acc, val) => (Object.assign({}, acc, { [val.name.value]: val.value.kind === 'StringValue' ? val.value.value : val.value.toString() })), {})
} })), {});
/**
* Given a potentially wrapping type, return the unwrapped type
*
* @param type A GraphQLType
* @returns A GraphQLNamedType (i.e. a type without List or NonNull)
*/
exports.unwrapType = type => {
if (graphql_1.isNamedType(type)) {
return type;
}
return exports.unwrapType(type.ofType);
};
/**
* This takes a {@link GraphQLOutputType} or {@link GraphQLInputType} and
* returns an IR TypeDefinition. In the case of a wrapping type (List/NonNull),
* this function will recurse.
*
* @param type The GraphQL Type
* @returns An IR TypeDefinition
*/
const convertTypeToIR = (type, nonNull = false) => {
if (graphql_1.isScalarType(type)) {
return {
kind: 'TypeDefinition',
nullable: !nonNull,
originalNode: null,
type: type.name,
isScalar: true,
};
}
else if (graphql_1.isObjectType(type)) {
return {
kind: 'TypeDefinition',
nullable: !nonNull,
originalNode: null,
type: type.name,
isScalar: false,
};
}
else if (graphql_1.isInterfaceType(type)) {
return {
kind: 'InterfaceTypeDefinition',
nullable: !nonNull,
originalNode: null,
};
}
else if (graphql_1.isUnionType(type)) {
return {
kind: 'TypeDefinition',
nullable: !nonNull,
originalNode: null,
type: type.name,
isScalar: false,
};
}
else if (graphql_1.isEnumType(type)) {
return {
kind: 'EnumTypeDefinition',
nullable: !nonNull,
originalNode: null,
type: type.name,
values: type.getValues().map(value => value.value)
};
}
else if (graphql_1.isListType(type)) {
return {
kind: 'ListTypeDefinition',
of: convertTypeToIR(type.ofType),
nullable: !nonNull,
originalNode: null
};
}
else if (graphql_1.isNonNullType(type)) {
return convertTypeToIR(type.ofType, true);
}
else if (graphql_1.isInputObjectType(type)) {
return {
kind: 'TypeDefinition',
originalNode: null,
nullable: !nonNull,
type: type.name,
isScalar: false
};
}
else {
throw new Error(`Unsupported Type: ${type}`);
}
};
/**
* Given a SelectionSetNode, return a list of SelectionNodes
* @param selectionSet A field's selection set
* @returns An array of SelectionNodes
*/
const extractSelections = selectionSet => selectionSet ? [...selectionSet.selections] : [];
/**
* Converts a FieldNode into the FieldDefinition IR
*
* This supports converting into a:
* - ITypenameNode (a `__typename` selection)
* - ILeafNode (a scalar selection)
* - IFieldNode (an object type)
*
* If we encounter a FieldNode selection, we recurse over its selection set
*
* @param fieldNode A FieldNode selection
* @param nodeType The {@link GraphQLNamedType} that the selection belongs to
* @param schema The GraphQL Schema
* @returns The FieldDefinition IR
*/
const convertFieldNodeToIR = (fieldNode, nodeType, schema) => {
const fieldName = fieldNode.name.value;
/**
* `__typename` (and other introspection fields) are special. They
* don't exist on the actual `GraphQLNamedType`. Additionally,
* `__typename` should be more than just a String type; it should be
* the type's name.
*/
if (fieldName === '__typename') {
return {
kind: 'TypenameNode',
typeDefinition: {
kind: 'TypenameDefinition',
nullable: false,
type: graphql_1.isAbstractType(nodeType)
? schema.getPossibleTypes(nodeType).map(x => x.name)
: nodeType.name
},
name: fieldNode.alias ? fieldNode.alias.value : '__typename'
};
}
// @TODO support introspection fields
if (fieldName.startsWith('__')) {
throw new Error('introspection not supported yet');
}
// Collect the field from the Type using the field's name
const field = graphql_1.isObjectType(nodeType) || graphql_1.isInterfaceType(nodeType)
? nodeType.getFields()[fieldName]
: null;
// Get the underlying type of the field we're looking at
const underlyingType = field.type;
const resolvedName = fieldNode.alias
? fieldNode.alias.value
: fieldName;
if (graphql_1.isLeafType(graphql_1.getNamedType(underlyingType))) {
return {
kind: 'LeafNode',
name: resolvedName,
originalNode: null,
directives: extractDirectives(fieldNode.directives),
typeDefinition: convertTypeToIR(underlyingType)
};
}
return {
kind: 'Field',
name: resolvedName,
originalNode: null,
typeDefinition: convertTypeToIR(underlyingType),
selections: underlyingType
? collectSelectionsFromNode(extractSelections(fieldNode.selectionSet), graphql_1.getNamedType(underlyingType), schema)
: [],
directives: extractDirectives(fieldNode.directives)
};
};
/**
* This function takes a FieldNode of type {@link GraphQLInterfaceType} and converts it into
* an {@link IInterfaceNode} type.
*
* Imagine a query like:
*
* ```graphql
* query GetStuffFromInterface {
* interfaceSelection {
* __typename
* id
*
* ... on TypeA {
* fieldA
* }
*
* # No Selection on TypeB
*
* ... on TypeC {
* fieldC
* }
* }
* }
* ```
*
* The field `interfaceSelection` is an interface which is implemented by `TypeA`, `TypeB`, and `TypeC`.
*
* In this case the `selection` parameter would be the `interfaceSelection` `FieldNode` and nodeType would be of
* type `InterfaceSelection` (or whatever it is in the schema).
*
* This function will:
* 1. Collect all of the common field selections (in this case: `__typename` & `id`)
* 2. Collect the unique fields per type (in this case, more or less: `{ TypeA: [fieldA], TypeB: [], TypeC: [fieldC] }`)
* 3. Expand the fragment selections into all of the possible implementing types (in this case: TypeA, TypeB, TypeC)
* 4. Combine the common fields & unique field selections for each implementing type
*
* This will essentially transform the above query into:
*
* ```graphql
* query GetStuffFromInterface {
* interfaceSelection { *
* ... on TypeA {
* __typename
* id
* fieldA
* }
*
* # TypeB Selection now exists
* ... on TypeB {
* __typename
* id
* }
*
* ... on TypeC {
* __typename
* id
* fieldC
* }
* }
* }
* ```
*
*
* @param selection A FieldNode of type {@link GraphQLInterfaceType}
* @param nodeType The {@link GraphQLInterfaceType} that the selection belongs to
* @param schema The GraphQL Schema
* @returns An IR node for an InterfaceNode
*/
const convertInterfaceToInterfaceIR = (selection, nodeType, schema) => {
if (!selection.selectionSet) {
throw new Error('Invalid Selection on Interface');
}
// Split the selection set into a list of common fields and a map from implementing type to InlineFragmentNode
const [commonFields, uniqueFieldTypeMap] = selection.selectionSet.selections.reduce(([fields, typeMap], sel) => {
if (sel.kind === 'Field') {
return [fields.concat(sel), typeMap];
}
if (sel.kind === 'InlineFragment') {
const subType = sel.typeCondition.name.value;
return [fields, Object.assign({}, typeMap, { [subType]: sel })];
}
throw new Error('Invalid FragmentSpread found encountered!');
}, [[], {}]);
const possibleTypes = schema.getPossibleTypes(exports.unwrapType(nodeType));
const possibleTypeMap = possibleTypes.reduce((acc, type) => (Object.assign({}, acc, { [type.name]: uniqueFieldTypeMap[type.name] || null })), {});
// Merge the common fields & the fragment's selections. Builds a map of type to Selection[]
const collectedTypeMap = Object.keys(possibleTypeMap).reduce((acc, type) => (Object.assign({}, acc, { [type]: collectSelectionsFromNode([
...commonFields,
...extractSelections(possibleTypeMap[type] ? possibleTypeMap[type].selectionSet : undefined)
], schema.getType(type), schema) })), {});
return {
kind: 'InterfaceNode',
name: selection.name.value,
directives: extractDirectives(selection.directives),
typeDefinition: convertTypeToIR(nodeType),
fragments: Object.entries(collectedTypeMap).map(([key, value]) => ({
kind: 'Fragment',
directives: extractDirectives(possibleTypeMap[key] ? possibleTypeMap[key].directives : undefined),
originalNode: null,
selections: value,
typeDefinition: convertTypeToIR(schema.getType(key))
}))
};
};
/**
* Converts a SelectionNode to an Selection IR object
* @param selection A SelectionNode from the GraphQL AST
* @param nodeType The {@link GraphQLNamedType} that the selection belongs to
* @param schema The GraphQL Schema
* @returns A Selection IR object
*/
const convertSelectionToIR = (selection, nodeType, schema) => {
/**
* Determine if a selection is an interface and short circuit
*/
if (graphql_1.isObjectType(nodeType) && selection.kind === 'Field') {
const possibleInterface = nodeType.getFields()[selection.name.value];
const unwrappedType = possibleInterface ? exports.unwrapType(possibleInterface.type) : null;
if (unwrappedType && graphql_1.isInterfaceType(unwrappedType)) {
return convertInterfaceToInterfaceIR(selection, possibleInterface.type, schema);
}
}
switch (selection.kind) {
case 'Field':
return convertFieldNodeToIR(selection, nodeType, schema);
case 'FragmentSpread':
case 'InlineFragment':
throw new Error(`${selection.kind} Must Be Inlined!`);
default:
throw new Error('Invalid Selection');
}
};
// const rootIntrospectionTypes: Map<string, string> = new Map([[ '__schema', '__Schema' ], [ '__type', '__Type' ]]);
/**
* Iterates over an array of {@link SelectionNode} objects and returns an IR object for them
* @TODO support introspection types other than __typename
* @param selections An array of Selection Nodes from the GraphQL AST
* @param nodeType The {@link GraphQLNamedType} that the selections belong to
* @param schema The GraphQL Schema
* @returns An array of Selection IR objects
*/
const collectSelectionsFromNode = (selections, nodeType, schema) => selections.map(selection => convertSelectionToIR(selection, nodeType, schema));
/**
* Gets the proper operation field
* @param schema A GraphQL Schema
* @param operation An operation type
*
* @returns The correct operation object
*/
const getOperationFields = (schema, operation) => {
switch (operation) {
case 'mutation':
return schema.getMutationType();
case 'subscription':
return schema.getSubscriptionType();
case 'query':
default:
return schema.getQueryType();
}
};
const extractVariables = (vars, _schema) => {
if (!vars || !vars.length) {
return [];
}
return [];
// return vars.map<IVariable>(v => ({
// kind: 'Variable',
// name: v.variable.name.value,
// originalNode: null!,
// type: convertTypeToIR(v.type as any)
// }));
};
/**
* Given a schema and a query, return an internal representation of the query
* @param schema A GraphQL Schema
* @param query A GraphQL Query
* @returns An internal representation of the query
*/
const convertToIr = (schema, query) => {
// TODO: remove index access
const def = query
.definitions[0];
const operationType = getOperationFields(schema, def.operation);
if (!operationType) {
throw new Error('Unsupported Operation');
}
const returnVal = {
kind: 'Root',
operationType: def.operation,
name: def.name ? def.name.value : undefined,
variables: extractVariables(def.variableDefinitions, schema),
directives: extractDirectives(def.directives),
selections: collectSelectionsFromNode(def.selectionSet.selections, operationType, schema)
};
return returnVal;
};
exports.default = convertToIr;
//# sourceMappingURL=ir.js.map