UNPKG

@gql2ts/from-query

Version:

generate typescript interfaces from a graphql schema and query

376 lines 13.4 kB
"use strict"; 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