UNPKG

@graphql-inspector/ci

Version:

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

248 lines (247 loc) • 12.6 kB
import { Kind, parseValue, print } from 'graphql'; import { AddedAttributeAlreadyExistsError, AddedAttributeCoordinateNotFoundError, AddedCoordinateAlreadyExistsError, ChangedAncestorCoordinateNotFoundError, ChangedCoordinateKindMismatchError, ChangedCoordinateNotFoundError, ChangePathMissingError, DeletedAncestorCoordinateNotFoundError, DeletedAttributeNotFoundError, ValueMismatchError, } from '../errors.js'; import { nameNode } from '../node-templates.js'; import { findNamedNode, parentPath } from '../utils.js'; /** * Tried to find the correct instance of the directive if it's repeated. * @note Should this should compare the arguments also to find the exact match if possible? */ function findNthDirective(directives, name, n) { let lastDirective; let count = 0; for (const d of directives) { // @note this nullish check is critical even though the types dont recognize it. if (d?.name.value === name) { lastDirective = d; count += 1; if (count === n) { break; } } } return lastDirective; } function directiveUsageDefinitionAdded(change, nodeByPath, config, _context) { if (!change.path) { config.onError(new ChangedCoordinateNotFoundError(Kind.DIRECTIVE, change.meta.addedDirectiveName), change); return; } const parentNode = nodeByPath.get(parentPath(change.path)); if (!parentNode) { config.onError(new ChangedAncestorCoordinateNotFoundError(change.path, change.type, change.meta.addedDirectiveName), change); return; } const definition = nodeByPath.get(`@${change.meta.addedDirectiveName}`); let repeatable = false; if (!definition) { console.warn(`Patch cannot determine the repeatability of directive "@${change.meta.addedDirectiveName}" because it's missing a definition.`); } if (definition?.kind === Kind.DIRECTIVE_DEFINITION) { repeatable = definition.repeatable; } const directiveNode = findNthDirective(parentNode?.directives ?? [], change.meta.addedDirectiveName, change.meta.directiveRepeatedTimes); if (!repeatable && directiveNode) { config.onError(new AddedCoordinateAlreadyExistsError(change.path, change.type), change); return; } const newDirective = { kind: Kind.DIRECTIVE, name: nameNode(change.meta.addedDirectiveName), }; parentNode.directives = [...(parentNode.directives ?? []), newDirective]; } function schemaDirectiveUsageDefinitionAdded(change, schemaNodes, nodeByPath, config, _context) { if (!change.path) { config.onError(new ChangedCoordinateNotFoundError(Kind.DIRECTIVE, change.meta.addedDirectiveName), change); return; } const definition = nodeByPath.get(`@${change.meta.addedDirectiveName}`); let repeatable = false; if (!definition) { console.warn(`Directive "@${change.meta.addedDirectiveName}" is missing a definition.`); } if (definition?.kind === Kind.DIRECTIVE_DEFINITION) { repeatable = definition.repeatable; } const directiveAlreadyExists = schemaNodes.some(schemaNode => findNthDirective(schemaNode.directives ?? [], change.meta.addedDirectiveName, change.meta.directiveRepeatedTimes)); if (!repeatable && directiveAlreadyExists) { config.onError(new AddedAttributeAlreadyExistsError(change.path, change.type, 'directives', change.meta.addedDirectiveName), change); return; } const directiveNode = { kind: Kind.DIRECTIVE, name: nameNode(change.meta.addedDirectiveName), }; schemaNodes[0].directives = [ ...(schemaNodes[0].directives ?? []), directiveNode, ]; } function schemaDirectiveUsageDefinitionRemoved(change, schemaNodes, _nodeByPath, config, _context) { let deleted = false; for (const node of schemaNodes) { const directiveNode = findNthDirective(node?.directives ?? [], change.meta.removedDirectiveName, change.meta.directiveRepeatedTimes); if (directiveNode) { node.directives = node.directives?.filter(d => d.name.value !== change.meta.removedDirectiveName); deleted = true; break; } } if (!deleted) { config.onError(new DeletedAttributeNotFoundError(change.path ?? '', change.type, 'directives', change.meta.removedDirectiveName), change); } } function directiveUsageDefinitionRemoved(change, nodeByPath, config, context) { if (!change.path) { config.onError(new ChangePathMissingError(change), change); return; } const parentNode = nodeByPath.get(parentPath(change.path)); if (!parentNode) { config.onError(new DeletedAncestorCoordinateNotFoundError(change.path, change.type, change.meta.removedDirectiveName), change); return; } const directiveNode = findNthDirective(parentNode?.directives ?? [], change.meta.removedDirectiveName, change.meta.directiveRepeatedTimes); if (!directiveNode) { config.onError(new DeletedAttributeNotFoundError(change.path, change.type, 'directives', change.meta.removedDirectiveName), change); return; } // null the value out for filtering later. The index is important so that changes reference // the correct DirectiveNode. // @note the nullish check is critical here even though the types dont show it const removedIndex = (parentNode.directives ?? []).findIndex(d => d === directiveNode); const directiveList = [...(parentNode.directives ?? [])]; if (removedIndex !== -1) { directiveList[removedIndex] = undefined; } parentNode.directives = directiveList; context.removedDirectiveNodes.push(parentNode); } export function directiveUsageArgumentDefinitionAdded(change, nodeByPath, config, context) { return directiveUsageDefinitionAdded(change, nodeByPath, config, context); } export function directiveUsageArgumentDefinitionRemoved(change, nodeByPath, config, context) { return directiveUsageDefinitionRemoved(change, nodeByPath, config, context); } export function directiveUsageEnumAdded(change, nodeByPath, config, context) { return directiveUsageDefinitionAdded(change, nodeByPath, config, context); } export function directiveUsageEnumRemoved(change, nodeByPath, config, context) { return directiveUsageDefinitionRemoved(change, nodeByPath, config, context); } export function directiveUsageEnumValueAdded(change, nodeByPath, config, context) { return directiveUsageDefinitionAdded(change, nodeByPath, config, context); } export function directiveUsageEnumValueRemoved(change, nodeByPath, config, context) { return directiveUsageDefinitionRemoved(change, nodeByPath, config, context); } export function directiveUsageFieldAdded(change, nodeByPath, config, context) { return directiveUsageDefinitionAdded(change, nodeByPath, config, context); } export function directiveUsageFieldDefinitionAdded(change, nodeByPath, config, context) { return directiveUsageDefinitionAdded(change, nodeByPath, config, context); } export function directiveUsageFieldDefinitionRemoved(change, nodeByPath, config, context) { return directiveUsageDefinitionRemoved(change, nodeByPath, config, context); } export function directiveUsageFieldRemoved(change, nodeByPath, config, context) { return directiveUsageDefinitionRemoved(change, nodeByPath, config, context); } export function directiveUsageInputFieldDefinitionAdded(change, nodeByPath, config, context) { return directiveUsageDefinitionAdded(change, nodeByPath, config, context); } export function directiveUsageInputFieldDefinitionRemoved(change, nodeByPath, config, context) { return directiveUsageDefinitionRemoved(change, nodeByPath, config, context); } export function directiveUsageInputObjectAdded(change, nodeByPath, config, context) { return directiveUsageDefinitionAdded(change, nodeByPath, config, context); } export function directiveUsageInputObjectRemoved(change, nodeByPath, config, context) { return directiveUsageDefinitionRemoved(change, nodeByPath, config, context); } export function directiveUsageInterfaceAdded(change, nodeByPath, config, context) { return directiveUsageDefinitionAdded(change, nodeByPath, config, context); } export function directiveUsageInterfaceRemoved(change, nodeByPath, config, context) { return directiveUsageDefinitionRemoved(change, nodeByPath, config, context); } export function directiveUsageObjectAdded(change, nodeByPath, config, context) { return directiveUsageDefinitionAdded(change, nodeByPath, config, context); } export function directiveUsageObjectRemoved(change, nodeByPath, config, context) { return directiveUsageDefinitionRemoved(change, nodeByPath, config, context); } export function directiveUsageScalarAdded(change, nodeByPath, config, context) { return directiveUsageDefinitionAdded(change, nodeByPath, config, context); } export function directiveUsageScalarRemoved(change, nodeByPath, config, context) { return directiveUsageDefinitionRemoved(change, nodeByPath, config, context); } export function directiveUsageSchemaAdded(change, schemaDefs, nodeByPath, config, context) { return schemaDirectiveUsageDefinitionAdded(change, schemaDefs, nodeByPath, config, context); } export function directiveUsageSchemaRemoved(change, schemaDefs, nodeByPath, config, context) { return schemaDirectiveUsageDefinitionRemoved(change, schemaDefs, nodeByPath, config, context); } export function directiveUsageUnionMemberAdded(change, nodeByPath, config, context) { return directiveUsageDefinitionAdded(change, nodeByPath, config, context); } export function directiveUsageUnionMemberRemoved(change, nodeByPath, config, context) { return directiveUsageDefinitionRemoved(change, nodeByPath, config, context); } export function directiveUsageArgumentAdded(change, nodeByPath, config, _context) { if (!change.path) { config.onError(new ChangePathMissingError(change), change); return; } // Must use double parentPath b/c the path is referencing the argument const parentNode = nodeByPath.get(parentPath(parentPath(change.path))); const directiveNode = findNthDirective(parentNode?.directives ?? [], change.meta.directiveName, change.meta.directiveRepeatedTimes); if (!directiveNode) { config.onError(new AddedAttributeCoordinateNotFoundError(change.path, change.type, change.meta.addedArgumentName), change); return; } if (directiveNode.kind !== Kind.DIRECTIVE) { config.onError(new ChangedCoordinateKindMismatchError(Kind.DIRECTIVE, directiveNode.kind), change); return; } const existing = findNamedNode(directiveNode.arguments, change.meta.addedArgumentName); // "ArgumentAdded" but argument already exists. if (existing) { config.onError(new ValueMismatchError(directiveNode.kind, null, print(existing.value)), change); existing.value = parseValue(change.meta.addedArgumentValue); return; } const argNode = { kind: Kind.ARGUMENT, name: nameNode(change.meta.addedArgumentName), value: parseValue(change.meta.addedArgumentValue), }; directiveNode.arguments = [ ...(directiveNode.arguments ?? []), argNode, ]; nodeByPath.set(change.path, argNode); } export function directiveUsageArgumentRemoved(change, nodeByPath, config, _context) { if (!change.path) { config.onError(new ChangePathMissingError(change), change); return; } // Must use double parentPath b/c the path is referencing the argument const parentNode = nodeByPath.get(parentPath(parentPath(change.path))); const directiveNode = findNthDirective(parentNode?.directives ?? [], change.meta.directiveName, change.meta.directiveRepeatedTimes); if (!directiveNode) { config.onError(new DeletedAncestorCoordinateNotFoundError(change.path, change.type, change.meta.removedArgumentName), change); return; } if (directiveNode.kind !== Kind.DIRECTIVE) { config.onError(new ChangedCoordinateKindMismatchError(Kind.DIRECTIVE, directiveNode.kind), change); return; } const existing = findNamedNode(directiveNode.arguments, change.meta.removedArgumentName); if (!existing) { config.onError(new DeletedAttributeNotFoundError(change.path, change.type, 'arguments', change.meta.removedArgumentName), change); } directiveNode.arguments = directiveNode.arguments?.filter(a => a.name.value !== change.meta.removedArgumentName); }