@graphql-hive/core
Version:
229 lines (228 loc) • 9.62 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.collectSchemaCoordinates = collectSchemaCoordinates;
const graphql_1 = require("graphql");
/** Collect all schema coordinates within a DocumentNode */
function collectSchemaCoordinates(args) {
const { variables } = args;
const entries = new Set();
const collected_entire_named_types = new Set();
const shouldAnalyzeVariableValues = args.processVariables === true && variables !== null;
function markAsUsed(id) {
if (!entries.has(id)) {
entries.add(id);
}
}
function makeId(...names) {
return names.join('.');
}
const collectedInputTypes = {};
function collectInputType(inputType, fieldName) {
if (!collectedInputTypes[inputType]) {
collectedInputTypes[inputType] = {
all: false,
fields: new Set(),
};
}
if (fieldName) {
collectedInputTypes[inputType].fields.add(fieldName);
}
else {
collectedInputTypes[inputType].all = true;
}
}
function collectNode(node) {
const inputType = args.typeInfo.getInputType();
if (!inputType) {
throw new Error('Expected an Input type, got nothing');
}
const inputTypeName = resolveTypeName(inputType);
if (node.value.kind === graphql_1.Kind.ENUM) {
// Collect only a specific enum value
collectInputType(inputTypeName, node.value.value);
}
else if (node.value.kind !== graphql_1.Kind.OBJECT && node.value.kind !== graphql_1.Kind.LIST) {
// When processing of variables is enabled,
// we want to skip collecting full input types of variables
// and only collect specific fields.
// That's why the following condition is added.
// Otherwise we would mark entire input types as used, and not granular fields.
if (node.value.kind === graphql_1.Kind.VARIABLE && shouldAnalyzeVariableValues) {
return;
}
collectInputType(inputTypeName);
}
}
function markEntireTypeAsUsed(type) {
const namedType = (0, graphql_1.getNamedType)(type);
if (collected_entire_named_types.has(namedType.name)) {
// No need to mark this type as used again
return;
}
// Add this type to the set of types that have been marked as used
// to avoid infinite loops
collected_entire_named_types.add(namedType.name);
if ((0, graphql_1.isScalarType)(namedType)) {
markAsUsed(makeId(namedType.name));
return;
}
if ((0, graphql_1.isEnumType)(namedType)) {
namedType.getValues().forEach(value => {
markAsUsed(makeId(namedType.name, value.name));
});
return;
}
const fieldsMap = namedType.getFields();
for (const fieldName in fieldsMap) {
const field = fieldsMap[fieldName];
markAsUsed(makeId(namedType.name, field.name));
markEntireTypeAsUsed(field.type);
}
}
function collectVariable(namedType, variableValue) {
const variableValueArray = Array.isArray(variableValue) ? variableValue : [variableValue];
if ((0, graphql_1.isInputObjectType)(namedType)) {
variableValueArray.forEach(variable => {
if (variable) {
// Collect only the used fields
for (const fieldName in variable) {
const field = namedType.getFields()[fieldName];
if (field) {
collectInputType(namedType.name, fieldName);
collectVariable((0, graphql_1.getNamedType)(field.type), variable[fieldName]);
}
}
}
else {
// Collect type without fields
markAsUsed(makeId(namedType.name));
}
});
}
else {
collectInputType(namedType.name);
}
}
(0, graphql_1.visit)(args.documentNode, (0, graphql_1.visitWithTypeInfo)(args.typeInfo, {
Field(node, _key, _parent, path, ancestors) {
const parent = args.typeInfo.getParentType();
const field = args.typeInfo.getFieldDef();
if (!parent) {
throw new Error(`Could not find a parent type of a field at ${printPath(path, ancestors, node.name)}`);
}
if (!field) {
throw new Error(`Could not find a field definition of a field at ${printPath(path, ancestors, node.name)}`);
}
markAsUsed(makeId(parent.name, field.name));
// Collect the entire type if it's an enum.
// Deleting an enum value that is used,
// should be a breaking change
// as it changes the output of the field.
const fieldType = (0, graphql_1.getNamedType)(field.type);
if (fieldType instanceof graphql_1.GraphQLEnumType) {
markEntireTypeAsUsed(fieldType);
}
},
VariableDefinition(node) {
const inputType = args.typeInfo.getInputType();
if (!inputType) {
throw new Error(`Could not find an input type of a variable $${node.variable.name}`);
}
if (shouldAnalyzeVariableValues) {
// Granular collection of variable values is enabled
const variableName = node.variable.name.value;
const variableValue = variables[variableName];
const namedType = (0, graphql_1.getNamedType)(inputType);
collectVariable(namedType, variableValue);
}
else {
// Collect the entire type without processing the variables
collectInputType(resolveTypeName(inputType));
}
},
Directive(node) {
return Object.assign(Object.assign({}, node), { arguments: [] });
},
Argument(node, _key, _parent, path, ancestors) {
const parent = args.typeInfo.getParentType();
const field = args.typeInfo.getFieldDef();
const arg = args.typeInfo.getArgument();
if (!parent) {
throw new Error(`Could not find a parent type of an argument at ${printPath(path, ancestors, node.name)}`);
}
if (!field) {
throw new Error(`Could not find a field definition of an argument at ${printPath(path, ancestors, node.name)}`);
}
if (!arg) {
throw new Error(`Could not find an argument definition of an argument at ${printPath(path, ancestors, node.name)}`);
}
markAsUsed(makeId(parent.name, field.name, arg.name));
collectNode(node);
},
ListValue(node, _key, _parent, path, ancestors) {
const inputType = args.typeInfo.getInputType();
if (!inputType) {
throw new Error(`Could not find an input type of a list value at ${printPath(path, ancestors)}`);
}
const inputTypeName = resolveTypeName(inputType);
node.values.forEach(value => {
if (value.kind === graphql_1.Kind.ENUM) {
collectInputType(inputTypeName, value.value);
}
else if (value.kind !== graphql_1.Kind.OBJECT) {
// if a value is not an object we need to collect all fields
collectInputType(inputTypeName);
}
});
},
ObjectField(node, _key, _parent, path, ancestors) {
const parentInputType = args.typeInfo.getParentInputType();
if (!parentInputType) {
throw new Error(`Could not find an input type of an object field at ${printPath(path, ancestors, node.name)}`);
}
const parentInputTypeName = resolveTypeName(parentInputType);
if ((0, graphql_1.isScalarType)(parentInputType)) {
collectInputType(parentInputTypeName);
// Prevent the visitor into going deeper into ObjectField
return null;
}
collectNode(node);
collectInputType(parentInputTypeName, node.name.value);
},
}));
for (const inputTypeName in collectedInputTypes) {
const { fields, all } = collectedInputTypes[inputTypeName];
if (all) {
markEntireTypeAsUsed(args.schema.getType(inputTypeName));
}
else {
fields.forEach(field => {
markAsUsed(makeId(inputTypeName, field));
});
}
}
return entries;
}
function resolveTypeName(inputType) {
return (0, graphql_1.getNamedType)(inputType).name;
}
function printPath(path, ancestors, leafNameNode) {
var _a;
const result = [];
for (let i = 0; i < path.length; i++) {
const key = path[i];
const ancestor = ancestors[i];
if (!ancestor) {
break;
}
if (key === 'selectionSet') {
if ('name' in ancestor && ((_a = ancestor.name) === null || _a === void 0 ? void 0 : _a.value)) {
result.push(ancestor.name.value);
}
}
}
if (leafNameNode) {
result.push(leafNameNode.value);
}
return result.join('.');
}
;