UNPKG

@graphql-inspector/cli

Version:

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

186 lines (185 loc) • 8.85 kB
import { Kind, parseConstValue, parseType, print, } from 'graphql'; import { AddedAttributeCoordinateNotFoundError, AddedCoordinateAlreadyExistsError, ChangedAncestorCoordinateNotFoundError, ChangedCoordinateKindMismatchError, ChangePathMissingError, DeletedAncestorCoordinateNotFoundError, DeletedAttributeNotFoundError, DeletedCoordinateNotFound, ValueMismatchError, } from '../errors.js'; import { nameNode, stringNode } from '../node-templates.js'; import { assertValueMatch, getChangedNodeOfKind, getDeletedNodeOfKind, parentPath, } from '../utils.js'; export function fieldTypeChanged(change, nodeByPath, config, _context) { const node = getChangedNodeOfKind(change, nodeByPath, Kind.FIELD_DEFINITION, config); if (node) { const currentReturnType = print(node.type); if (change.meta.oldFieldType !== currentReturnType) { config.onError(new ValueMismatchError(Kind.FIELD_DEFINITION, change.meta.oldFieldType, currentReturnType), change); } node.type = parseType(change.meta.newFieldType); } } export function fieldRemoved(change, nodeByPath, config, _context) { if (!change.path) { config.onError(new ChangePathMissingError(change), change); return; } const typeNode = nodeByPath.get(parentPath(change.path)); if (!typeNode) { config.onError(new DeletedAncestorCoordinateNotFoundError(change.path, change.type, change.meta.removedFieldName), change); return; } const beforeLength = typeNode.fields?.length ?? 0; typeNode.fields = typeNode.fields?.filter(f => f.name.value !== change.meta.removedFieldName); if (beforeLength === (typeNode.fields?.length ?? 0)) { config.onError(new DeletedAttributeNotFoundError(change.path, change.type, 'fields', change.meta.removedFieldName), change); } else { // delete the reference to the removed field. nodeByPath.delete(change.path); } } export function fieldAdded(change, nodeByPath, config, _context) { if (!change.path) { config.onError(new ChangePathMissingError(change), change); return; } const changedNode = nodeByPath.get(change.path); if (changedNode) { if (changedNode.kind === Kind.FIELD_DEFINITION) { if (print(changedNode.type) === change.meta.addedFieldReturnType) { config.onError(new AddedCoordinateAlreadyExistsError(change.path, change.type), change); } else { config.onError(new ValueMismatchError(Kind.FIELD_DEFINITION, undefined, change.meta.addedFieldReturnType), change); } } else { config.onError(new ChangedCoordinateKindMismatchError(Kind.FIELD_DEFINITION, changedNode.kind), change); } return; } const typeNode = nodeByPath.get(parentPath(change.path)); if (!typeNode) { config.onError(new ChangedAncestorCoordinateNotFoundError(change.path, change.type, change.meta.addedFieldName), change); return; } if (typeNode.kind !== Kind.OBJECT_TYPE_DEFINITION && typeNode.kind !== Kind.INTERFACE_TYPE_DEFINITION) { config.onError(new ChangedCoordinateKindMismatchError(Kind.ENUM_TYPE_DEFINITION, typeNode.kind), change); return; } const node = { kind: Kind.FIELD_DEFINITION, name: nameNode(change.meta.addedFieldName), type: parseType(change.meta.addedFieldReturnType), // description: change.meta.addedFieldDescription // ? stringNode(change.meta.addedFieldDescription) // : undefined, }; typeNode.fields = [...(typeNode.fields ?? []), node]; // add new field to the node set nodeByPath.set(change.path, node); } export function fieldArgumentAdded(change, nodeByPath, config, _context) { if (!change.path) { config.onError(new ChangePathMissingError(change), change); return; } const existing = nodeByPath.get(change.path); if (existing) { config.onError(new AddedCoordinateAlreadyExistsError(change.path, change.type), change); return; } const fieldNode = nodeByPath.get(parentPath(change.path)); if (!fieldNode) { config.onError(new AddedAttributeCoordinateNotFoundError(change.path, change.type, change.meta.addedArgumentName), change); return; } if (fieldNode.kind !== Kind.FIELD_DEFINITION) { config.onError(new ChangedCoordinateKindMismatchError(Kind.FIELD_DEFINITION, fieldNode.kind), change); return; } const node = { kind: Kind.INPUT_VALUE_DEFINITION, name: nameNode(change.meta.addedArgumentName), type: parseType(change.meta.addedArgumentType), // description: change.meta.addedArgumentDescription // ? stringNode(change.meta.addedArgumentDescription) // : undefined, }; fieldNode.arguments = [...(fieldNode.arguments ?? []), node]; // add new field to the node set nodeByPath.set(change.path, node); } export function fieldArgumentTypeChanged(change, nodeByPath, config, _context) { const existingArg = getChangedNodeOfKind(change, nodeByPath, Kind.INPUT_VALUE_DEFINITION, config); if (existingArg) { assertValueMatch(change, Kind.INPUT_VALUE_DEFINITION, change.meta.oldArgumentType, print(existingArg.type), config); existingArg.type = parseType(change.meta.newArgumentType); } } export function fieldArgumentDescriptionChanged(change, nodeByPath, config, _context) { const existingArg = getChangedNodeOfKind(change, nodeByPath, Kind.INPUT_VALUE_DEFINITION, config); if (existingArg) { assertValueMatch(change, Kind.INPUT_VALUE_DEFINITION, change.meta.oldDescription ?? undefined, existingArg.description?.value, config); existingArg.description = change.meta.newDescription ? stringNode(change.meta.newDescription) : undefined; } } export function fieldArgumentDefaultChanged(change, nodeByPath, config, _context) { const existingArg = getChangedNodeOfKind(change, nodeByPath, Kind.INPUT_VALUE_DEFINITION, config); if (existingArg) { assertValueMatch(change, Kind.INPUT_VALUE_DEFINITION, change.meta.oldDefaultValue, existingArg.defaultValue && print(existingArg.defaultValue), config); existingArg.defaultValue = change.meta.newDefaultValue ? parseConstValue(change.meta.newDefaultValue) : undefined; } } export function fieldArgumentRemoved(change, nodeByPath, config, _context) { const existing = getDeletedNodeOfKind(change, nodeByPath, Kind.ARGUMENT, config); if (!existing) { config.onError(new DeletedCoordinateNotFound(change.path ?? '', change.type), change); return; } const fieldNode = nodeByPath.get(parentPath(change.path)); if (!fieldNode) { config.onError(new DeletedAncestorCoordinateNotFoundError(change.path, // asserted by "getDeletedNodeOfKind" change.type, change.meta.removedFieldArgumentName), change); return; } if (fieldNode.kind !== Kind.FIELD_DEFINITION) { config.onError(new ChangedCoordinateKindMismatchError(Kind.FIELD_DEFINITION, fieldNode.kind), change); } fieldNode.arguments = fieldNode.arguments?.filter(a => a.name.value === change.meta.removedFieldArgumentName); // add new field to the node set nodeByPath.delete(change.path); } export function fieldDescriptionAdded(change, nodeByPath, config, _context) { const fieldNode = getChangedNodeOfKind(change, nodeByPath, Kind.FIELD_DEFINITION, config); if (fieldNode) { fieldNode.description = change.meta.addedDescription ? stringNode(change.meta.addedDescription) : undefined; } } export function fieldDescriptionRemoved(change, nodeByPath, config, _context) { if (!change.path) { config.onError(new ChangePathMissingError(change), change); return; } const fieldNode = nodeByPath.get(change.path); if (!fieldNode) { config.onError(new DeletedCoordinateNotFound(change.path, change.type), change); return; } if (fieldNode.kind !== Kind.FIELD_DEFINITION) { config.onError(new ChangedCoordinateKindMismatchError(Kind.FIELD_DEFINITION, fieldNode.kind), change); return; } fieldNode.description = undefined; } export function fieldDescriptionChanged(change, nodeByPath, config, _context) { const fieldNode = getChangedNodeOfKind(change, nodeByPath, Kind.FIELD_DEFINITION, config); if (!fieldNode) { return; } if (fieldNode.description?.value !== change.meta.oldDescription) { config.onError(new ValueMismatchError(Kind.FIELD_DEFINITION, change.meta.oldDescription, fieldNode.description?.value), change); } fieldNode.description = stringNode(change.meta.newDescription); }