@graphql-inspector/cli
Version:
Tooling for GraphQL. Compare GraphQL Schemas, check documents, find breaking changes, find similar types.
248 lines (247 loc) • 12.6 kB
JavaScript
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);
}