@graphql-tools/utils
Version:
Common package containing utils and types for GraphQL tools
253 lines (252 loc) • 10.7 kB
JavaScript
import { getNullableType, isAbstractType, isListType, isObjectType, Kind, SchemaMetaFieldDef, TypeMetaFieldDef, TypeNameMetaFieldDef, } from 'graphql';
import { collectFields, collectSubFields } from './collectFields.js';
import { getOperationASTFromRequest } from './getOperationASTFromRequest.js';
export function visitData(data, enter, leave) {
if (Array.isArray(data)) {
return data.map(value => visitData(value, enter, leave));
}
else if (typeof data === 'object') {
const newData = enter != null ? enter(data) : data;
if (newData != null) {
for (const key in newData) {
const value = newData[key];
Object.defineProperty(newData, key, {
value: visitData(value, enter, leave),
});
}
}
return leave != null ? leave(newData) : newData;
}
return data;
}
export function visitErrors(errors, visitor) {
return errors.map(error => visitor(error));
}
export function visitResult(result, request, schema, resultVisitorMap, errorVisitorMap) {
const fragments = request.document.definitions.reduce((acc, def) => {
if (def.kind === Kind.FRAGMENT_DEFINITION) {
acc[def.name.value] = def;
}
return acc;
}, {});
const variableValues = request.variables || {};
const errorInfo = {
segmentInfoMap: new Map(),
unpathedErrors: new Set(),
};
const data = result.data;
const errors = result.errors;
const visitingErrors = errors != null && errorVisitorMap != null;
const operationDocumentNode = getOperationASTFromRequest(request);
if (data != null && operationDocumentNode != null) {
result.data = visitRoot(data, operationDocumentNode, schema, fragments, variableValues, resultVisitorMap, visitingErrors ? errors : undefined, errorInfo);
}
if (errors != null && errorVisitorMap) {
result.errors = visitErrorsByType(errors, errorVisitorMap, errorInfo);
}
return result;
}
function visitErrorsByType(errors, errorVisitorMap, errorInfo) {
const segmentInfoMap = errorInfo.segmentInfoMap;
const unpathedErrors = errorInfo.unpathedErrors;
const unpathedErrorVisitor = errorVisitorMap['__unpathed'];
return errors.map(originalError => {
const pathSegmentsInfo = segmentInfoMap.get(originalError);
const newError = pathSegmentsInfo == null
? originalError
: pathSegmentsInfo.reduceRight((acc, segmentInfo) => {
const typeName = segmentInfo.type.name;
const typeVisitorMap = errorVisitorMap[typeName];
if (typeVisitorMap == null) {
return acc;
}
const errorVisitor = typeVisitorMap[segmentInfo.fieldName];
return errorVisitor == null ? acc : errorVisitor(acc, segmentInfo.pathIndex);
}, originalError);
if (unpathedErrorVisitor && unpathedErrors.has(originalError)) {
return unpathedErrorVisitor(newError);
}
return newError;
});
}
function getOperationRootType(schema, operationDef) {
switch (operationDef.operation) {
case 'query':
return schema.getQueryType();
case 'mutation':
return schema.getMutationType();
case 'subscription':
return schema.getSubscriptionType();
}
}
function visitRoot(root, operation, schema, fragments, variableValues, resultVisitorMap, errors, errorInfo) {
const operationRootType = getOperationRootType(schema, operation);
const { fields: collectedFields } = collectFields(schema, fragments, variableValues, operationRootType, operation.selectionSet);
return visitObjectValue(root, operationRootType, collectedFields, schema, fragments, variableValues, resultVisitorMap, 0, errors, errorInfo);
}
function visitObjectValue(object, type, fieldNodeMap, schema, fragments, variableValues, resultVisitorMap, pathIndex, errors, errorInfo) {
const fieldMap = type.getFields();
const typeVisitorMap = resultVisitorMap?.[type.name];
const enterObject = typeVisitorMap?.__enter;
const newObject = enterObject != null ? enterObject(object) : object;
let sortedErrors;
let errorMap = null;
if (errors != null) {
sortedErrors = sortErrorsByPathSegment(errors, pathIndex);
errorMap = sortedErrors.errorMap;
for (const error of sortedErrors.unpathedErrors) {
errorInfo.unpathedErrors.add(error);
}
}
for (const [responseKey, subFieldNodes] of fieldNodeMap) {
const fieldName = subFieldNodes[0].name.value;
let fieldType = fieldMap[fieldName]?.type;
if (fieldType == null) {
switch (fieldName) {
case '__typename':
fieldType = TypeNameMetaFieldDef.type;
break;
case '__schema':
fieldType = SchemaMetaFieldDef.type;
break;
case '__type':
fieldType = TypeMetaFieldDef.type;
break;
}
}
const newPathIndex = pathIndex + 1;
let fieldErrors;
if (errorMap) {
fieldErrors = errorMap[responseKey];
if (fieldErrors != null) {
delete errorMap[responseKey];
}
addPathSegmentInfo(type, fieldName, newPathIndex, fieldErrors, errorInfo);
}
const newValue = visitFieldValue(object[responseKey], fieldType, subFieldNodes, schema, fragments, variableValues, resultVisitorMap, newPathIndex, fieldErrors, errorInfo);
updateObject(newObject, responseKey, newValue, typeVisitorMap, fieldName);
}
const oldTypename = newObject.__typename;
if (oldTypename != null) {
updateObject(newObject, '__typename', oldTypename, typeVisitorMap, '__typename');
}
if (errorMap) {
for (const errorsKey in errorMap) {
const errors = errorMap[errorsKey];
for (const error of errors) {
errorInfo.unpathedErrors.add(error);
}
}
}
const leaveObject = typeVisitorMap?.__leave;
return leaveObject != null ? leaveObject(newObject) : newObject;
}
function updateObject(object, responseKey, newValue, typeVisitorMap, fieldName) {
if (typeVisitorMap == null) {
object[responseKey] = newValue;
return;
}
const fieldVisitor = typeVisitorMap[fieldName];
if (fieldVisitor == null) {
object[responseKey] = newValue;
return;
}
const visitedValue = fieldVisitor(newValue);
if (visitedValue === undefined) {
delete object[responseKey];
return;
}
object[responseKey] = visitedValue;
}
function visitListValue(list, returnType, fieldNodes, schema, fragments, variableValues, resultVisitorMap, pathIndex, errors, errorInfo) {
return list.map(listMember => visitFieldValue(listMember, returnType, fieldNodes, schema, fragments, variableValues, resultVisitorMap, pathIndex + 1, errors, errorInfo));
}
function visitFieldValue(value, returnType, fieldNodes, schema, fragments, variableValues, resultVisitorMap, pathIndex, errors = [], errorInfo) {
if (value == null) {
return value;
}
const nullableType = getNullableType(returnType);
if (isListType(nullableType)) {
return visitListValue(value, nullableType.ofType, fieldNodes, schema, fragments, variableValues, resultVisitorMap, pathIndex, errors, errorInfo);
}
else if (isAbstractType(nullableType)) {
const finalType = schema.getType(value.__typename);
let { fields: collectedFields, patches } = collectSubFields(schema, fragments, variableValues, finalType, fieldNodes);
if (patches.length) {
collectedFields = new Map(collectedFields);
for (const patch of patches) {
for (const [responseKey, fields] of patch.fields) {
const existingFields = collectedFields.get(responseKey);
if (existingFields) {
existingFields.push(...fields);
}
else {
collectedFields.set(responseKey, fields);
}
}
}
}
return visitObjectValue(value, finalType, collectedFields, schema, fragments, variableValues, resultVisitorMap, pathIndex, errors, errorInfo);
}
else if (isObjectType(nullableType)) {
let { fields: collectedFields, patches } = collectSubFields(schema, fragments, variableValues, nullableType, fieldNodes);
if (patches.length) {
collectedFields = new Map(collectedFields);
for (const patch of patches) {
for (const [responseKey, fields] of patch.fields) {
const existingFields = collectedFields.get(responseKey);
if (existingFields) {
existingFields.push(...fields);
}
else {
collectedFields.set(responseKey, fields);
}
}
}
}
return visitObjectValue(value, nullableType, collectedFields, schema, fragments, variableValues, resultVisitorMap, pathIndex, errors, errorInfo);
}
const typeVisitorMap = resultVisitorMap?.[nullableType.name];
if (typeVisitorMap == null) {
return value;
}
const visitedValue = typeVisitorMap(value);
return visitedValue === undefined ? value : visitedValue;
}
function sortErrorsByPathSegment(errors, pathIndex) {
const errorMap = Object.create(null);
const unpathedErrors = new Set();
for (const error of errors) {
const pathSegment = error.path?.[pathIndex];
if (pathSegment == null) {
unpathedErrors.add(error);
continue;
}
if (pathSegment in errorMap) {
errorMap[pathSegment].push(error);
}
else {
errorMap[pathSegment] = [error];
}
}
return {
errorMap,
unpathedErrors,
};
}
function addPathSegmentInfo(type, fieldName, pathIndex, errors = [], errorInfo) {
for (const error of errors) {
const segmentInfo = {
type,
fieldName,
pathIndex,
};
const pathSegmentsInfo = errorInfo.segmentInfoMap.get(error);
if (pathSegmentsInfo == null) {
errorInfo.segmentInfoMap.set(error, [segmentInfo]);
}
else {
pathSegmentsInfo.push(segmentInfo);
}
}
}