UNPKG

@graphql-inspector/cli

Version:

Tooling for GraphQL. Compare GraphQL Schemas, check documents, find breaking changes, find similar types.

447 lines (446 loc) • 21.5 kB
import { buildASTSchema, isDefinitionNode, Kind, parse, visit, } from 'graphql'; import { ChangeType } from '@graphql-inspector/core'; import { printSchemaWithDirectives } from '@graphql-tools/utils'; import { defaultErrorHandler } from './errors.js'; import { directiveUsageArgumentAdded, directiveUsageArgumentDefinitionAdded, directiveUsageArgumentDefinitionRemoved, directiveUsageArgumentRemoved, directiveUsageEnumAdded, directiveUsageEnumRemoved, directiveUsageEnumValueAdded, directiveUsageEnumValueRemoved, directiveUsageFieldAdded, directiveUsageFieldDefinitionAdded, directiveUsageFieldDefinitionRemoved, directiveUsageFieldRemoved, directiveUsageInputFieldDefinitionAdded, directiveUsageInputFieldDefinitionRemoved, directiveUsageInputObjectAdded, directiveUsageInputObjectRemoved, directiveUsageInterfaceAdded, directiveUsageInterfaceRemoved, directiveUsageObjectAdded, directiveUsageObjectRemoved, directiveUsageScalarAdded, directiveUsageScalarRemoved, directiveUsageSchemaAdded, directiveUsageSchemaRemoved, directiveUsageUnionMemberAdded, directiveUsageUnionMemberRemoved, } from './patches/directive-usages.js'; import { directiveAdded, directiveArgumentAdded, directiveArgumentDefaultValueChanged, directiveArgumentDescriptionChanged, directiveArgumentRemoved, directiveArgumentTypeChanged, directiveDescriptionChanged, directiveLocationAdded, directiveLocationRemoved, directiveRemoved, directiveRepeatableAdded, directiveRepeatableRemoved, } from './patches/directives.js'; import { enumValueAdded, enumValueDescriptionChanged, enumValueRemoved } from './patches/enum.js'; import { fieldAdded, fieldArgumentAdded, fieldArgumentDefaultChanged, fieldArgumentDescriptionChanged, fieldArgumentRemoved, fieldArgumentTypeChanged, fieldDescriptionAdded, fieldDescriptionChanged, fieldDescriptionRemoved, fieldRemoved, fieldTypeChanged, } from './patches/fields.js'; import { inputFieldAdded, inputFieldDefaultValueChanged, inputFieldDescriptionAdded, inputFieldDescriptionChanged, inputFieldDescriptionRemoved, inputFieldRemoved, inputFieldTypeChanged, } from './patches/inputs.js'; import { objectTypeInterfaceAdded, objectTypeInterfaceRemoved } from './patches/interfaces.js'; import { schemaMutationTypeChanged, schemaQueryTypeChanged, schemaSubscriptionTypeChanged, } from './patches/schema.js'; import { typeAdded, typeDescriptionAdded, typeDescriptionChanged, typeDescriptionRemoved, typeRemoved, } from './patches/types.js'; import { unionMemberAdded, unionMemberRemoved } from './patches/unions.js'; import { debugPrintChange } from './utils.js'; export * as errors from './errors.js'; /** * Wraps converting a schema to AST safely, patching, then rebuilding the schema from AST. * The schema is not validated in this function. That it is the responsibility of the caller. */ export function patchSchema(schema, changes, config) { const ast = parse(printSchemaWithDirectives(schema, { assumeValid: true })); const patchedAst = patch(ast, changes, config); return buildASTSchema(patchedAst, { assumeValid: true, assumeValidSDL: true }); } /** * Extracts all the root definitions from a DocumentNode and creates a mapping of their coordinate * to the defined ASTNode. E.g. A field's coordinate is "Type.field". */ export function groupByCoordinateAST(ast) { const schemaNodes = []; const nodesByCoordinate = new Map(); const pathArray = []; visit(ast, { enter(node, key) { switch (node.kind) { case Kind.ARGUMENT: case Kind.ENUM_TYPE_DEFINITION: case Kind.ENUM_TYPE_EXTENSION: case Kind.ENUM_VALUE_DEFINITION: case Kind.FIELD_DEFINITION: case Kind.INPUT_OBJECT_TYPE_DEFINITION: case Kind.INPUT_OBJECT_TYPE_EXTENSION: case Kind.INPUT_VALUE_DEFINITION: case Kind.INTERFACE_TYPE_DEFINITION: case Kind.INTERFACE_TYPE_EXTENSION: case Kind.OBJECT_FIELD: case Kind.OBJECT_TYPE_DEFINITION: case Kind.OBJECT_TYPE_EXTENSION: case Kind.SCALAR_TYPE_DEFINITION: case Kind.SCALAR_TYPE_EXTENSION: case Kind.UNION_TYPE_DEFINITION: case Kind.UNION_TYPE_EXTENSION: { pathArray.push(node.name.value); const path = pathArray.join('.'); nodesByCoordinate.set(path, node); break; } case Kind.DIRECTIVE_DEFINITION: { pathArray.push(`@${node.name.value}`); const path = pathArray.join('.'); nodesByCoordinate.set(path, node); break; } case Kind.DIRECTIVE: { /** * Check if this directive is on the schema node. If so, then push an empty path * to distinguish it from the definitions */ const isRoot = pathArray.length === 0; if (isRoot) { pathArray.push(`.@${node.name.value}[${key}]`); } else { pathArray.push(`@${node.name.value}[${key}]`); } // const path = pathArray.join('.'); // nodesByCoordinate.set(path, node); // @note skip setting the node for directives because repeat directives screw this up. break; } case Kind.DOCUMENT: { break; } case Kind.SCHEMA_EXTENSION: case Kind.SCHEMA_DEFINITION: { // @todo There can be only one. Replace `schemaNodes` with using `nodesByCoordinate.get('')`. schemaNodes.push(node); nodesByCoordinate.set('', node); break; } // default: { // // by definition this things like return types, names, named nodes... // // it's nothing we want to collect. // return false; // } } }, leave(node) { switch (node.kind) { case Kind.ARGUMENT: case Kind.ENUM_TYPE_DEFINITION: case Kind.ENUM_TYPE_EXTENSION: case Kind.ENUM_VALUE_DEFINITION: case Kind.FIELD_DEFINITION: case Kind.INPUT_OBJECT_TYPE_DEFINITION: case Kind.INPUT_OBJECT_TYPE_EXTENSION: case Kind.INPUT_VALUE_DEFINITION: case Kind.INTERFACE_TYPE_DEFINITION: case Kind.INTERFACE_TYPE_EXTENSION: case Kind.OBJECT_FIELD: case Kind.OBJECT_TYPE_DEFINITION: case Kind.OBJECT_TYPE_EXTENSION: case Kind.SCALAR_TYPE_DEFINITION: case Kind.SCALAR_TYPE_EXTENSION: case Kind.UNION_TYPE_DEFINITION: case Kind.UNION_TYPE_EXTENSION: case Kind.DIRECTIVE_DEFINITION: case Kind.DIRECTIVE: { pathArray.pop(); break; } } }, }); return [schemaNodes, nodesByCoordinate]; } export function patchCoordinatesAST(schemaNodes, nodesByCoordinate, changes, patchConfig = {}) { const config = { onError: defaultErrorHandler, debug: false, ...patchConfig, }; const context = { removedDirectiveNodes: [], }; for (const change of changes) { if (config.debug) { debugPrintChange(change, nodesByCoordinate); } switch (change.type) { case ChangeType.SchemaMutationTypeChanged: { schemaMutationTypeChanged(change, schemaNodes, config, context); break; } case ChangeType.SchemaQueryTypeChanged: { schemaQueryTypeChanged(change, schemaNodes, config, context); break; } case ChangeType.SchemaSubscriptionTypeChanged: { schemaSubscriptionTypeChanged(change, schemaNodes, config, context); break; } case ChangeType.DirectiveAdded: { directiveAdded(change, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveRemoved: { directiveRemoved(change, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveArgumentAdded: { directiveArgumentAdded(change, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveArgumentRemoved: { directiveArgumentRemoved(change, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveLocationAdded: { directiveLocationAdded(change, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveLocationRemoved: { directiveLocationRemoved(change, nodesByCoordinate, config, context); break; } case ChangeType.EnumValueAdded: { enumValueAdded(change, nodesByCoordinate, config, context); break; } case ChangeType.FieldAdded: { fieldAdded(change, nodesByCoordinate, config, context); break; } case ChangeType.FieldRemoved: { fieldRemoved(change, nodesByCoordinate, config, context); break; } case ChangeType.FieldTypeChanged: { fieldTypeChanged(change, nodesByCoordinate, config, context); break; } case ChangeType.FieldArgumentAdded: { fieldArgumentAdded(change, nodesByCoordinate, config, context); break; } case ChangeType.FieldArgumentTypeChanged: { fieldArgumentTypeChanged(change, nodesByCoordinate, config, context); break; } case ChangeType.FieldArgumentRemoved: { fieldArgumentRemoved(change, nodesByCoordinate, config, context); break; } case ChangeType.FieldArgumentDescriptionChanged: { fieldArgumentDescriptionChanged(change, nodesByCoordinate, config, context); break; } case ChangeType.FieldArgumentDefaultChanged: { fieldArgumentDefaultChanged(change, nodesByCoordinate, config, context); break; } case ChangeType.FieldDescriptionAdded: { fieldDescriptionAdded(change, nodesByCoordinate, config, context); break; } case ChangeType.FieldDescriptionChanged: { fieldDescriptionChanged(change, nodesByCoordinate, config, context); break; } case ChangeType.InputFieldAdded: { inputFieldAdded(change, nodesByCoordinate, config, context); break; } case ChangeType.InputFieldRemoved: { inputFieldRemoved(change, nodesByCoordinate, config, context); break; } case ChangeType.InputFieldDescriptionAdded: { inputFieldDescriptionAdded(change, nodesByCoordinate, config, context); break; } case ChangeType.InputFieldTypeChanged: { inputFieldTypeChanged(change, nodesByCoordinate, config, context); break; } case ChangeType.InputFieldDescriptionChanged: { inputFieldDescriptionChanged(change, nodesByCoordinate, config, context); break; } case ChangeType.InputFieldDescriptionRemoved: { inputFieldDescriptionRemoved(change, nodesByCoordinate, config, context); break; } case ChangeType.InputFieldDefaultValueChanged: { inputFieldDefaultValueChanged(change, nodesByCoordinate, config, context); break; } case ChangeType.ObjectTypeInterfaceAdded: { objectTypeInterfaceAdded(change, nodesByCoordinate, config, context); break; } case ChangeType.ObjectTypeInterfaceRemoved: { objectTypeInterfaceRemoved(change, nodesByCoordinate, config, context); break; } case ChangeType.TypeDescriptionAdded: { typeDescriptionAdded(change, nodesByCoordinate, config, context); break; } case ChangeType.TypeDescriptionChanged: { typeDescriptionChanged(change, nodesByCoordinate, config, context); break; } case ChangeType.TypeDescriptionRemoved: { typeDescriptionRemoved(change, nodesByCoordinate, config, context); break; } case ChangeType.TypeAdded: { typeAdded(change, nodesByCoordinate, config, context); break; } case ChangeType.UnionMemberAdded: { unionMemberAdded(change, nodesByCoordinate, config, context); break; } case ChangeType.UnionMemberRemoved: { unionMemberRemoved(change, nodesByCoordinate, config, context); break; } case ChangeType.TypeRemoved: { typeRemoved(change, nodesByCoordinate, config, context); break; } case ChangeType.EnumValueRemoved: { enumValueRemoved(change, nodesByCoordinate, config, context); break; } case ChangeType.EnumValueDescriptionChanged: { enumValueDescriptionChanged(change, nodesByCoordinate, config, context); break; } case ChangeType.FieldDescriptionRemoved: { fieldDescriptionRemoved(change, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveArgumentDefaultValueChanged: { directiveArgumentDefaultValueChanged(change, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveArgumentDescriptionChanged: { directiveArgumentDescriptionChanged(change, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveArgumentTypeChanged: { directiveArgumentTypeChanged(change, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveDescriptionChanged: { directiveDescriptionChanged(change, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveRepeatableAdded: { directiveRepeatableAdded(change, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveRepeatableRemoved: { directiveRepeatableRemoved(change, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveUsageArgumentDefinitionAdded: { directiveUsageArgumentDefinitionAdded(change, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveUsageArgumentDefinitionRemoved: { directiveUsageArgumentDefinitionRemoved(change, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveUsageEnumAdded: { directiveUsageEnumAdded(change, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveUsageEnumRemoved: { directiveUsageEnumRemoved(change, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveUsageEnumValueAdded: { directiveUsageEnumValueAdded(change, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveUsageEnumValueRemoved: { directiveUsageEnumValueRemoved(change, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveUsageFieldAdded: { directiveUsageFieldAdded(change, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveUsageFieldDefinitionAdded: { directiveUsageFieldDefinitionAdded(change, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveUsageFieldDefinitionRemoved: { directiveUsageFieldDefinitionRemoved(change, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveUsageFieldRemoved: { directiveUsageFieldRemoved(change, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveUsageInputFieldDefinitionAdded: { directiveUsageInputFieldDefinitionAdded(change, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveUsageInputFieldDefinitionRemoved: { directiveUsageInputFieldDefinitionRemoved(change, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveUsageInputObjectAdded: { directiveUsageInputObjectAdded(change, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveUsageInputObjectRemoved: { directiveUsageInputObjectRemoved(change, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveUsageInterfaceAdded: { directiveUsageInterfaceAdded(change, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveUsageInterfaceRemoved: { directiveUsageInterfaceRemoved(change, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveUsageObjectAdded: { directiveUsageObjectAdded(change, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveUsageObjectRemoved: { directiveUsageObjectRemoved(change, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveUsageScalarAdded: { directiveUsageScalarAdded(change, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveUsageScalarRemoved: { directiveUsageScalarRemoved(change, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveUsageSchemaAdded: { directiveUsageSchemaAdded(change, schemaNodes, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveUsageSchemaRemoved: { directiveUsageSchemaRemoved(change, schemaNodes, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveUsageUnionMemberAdded: { directiveUsageUnionMemberAdded(change, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveUsageUnionMemberRemoved: { directiveUsageUnionMemberRemoved(change, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveUsageArgumentAdded: { directiveUsageArgumentAdded(change, nodesByCoordinate, config, context); break; } case ChangeType.DirectiveUsageArgumentRemoved: { directiveUsageArgumentRemoved(change, nodesByCoordinate, config, context); break; } default: { console.log(`${change.type} is not implemented yet.`); } } } for (const node of context.removedDirectiveNodes) { node.directives = node.directives?.filter(d => d != null); } return { kind: Kind.DOCUMENT, // filter out the non-definition nodes (e.g. field definitions) definitions: [ ...schemaNodes, ...Array.from(nodesByCoordinate.values()).filter(isDefinitionNode), ], }; } /** This method wraps groupByCoordinateAST and patchCoordinatesAST for convenience. */ export function patch(ast, changes, patchConfig) { const [schemaNodes, nodesByCoordinate] = groupByCoordinateAST(ast); return patchCoordinatesAST(schemaNodes, nodesByCoordinate, changes, patchConfig); }