graphql-join
Version:
Join types together in your schema declaratively with SDL.
235 lines • 12.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateFieldConfig = void 0;
const graphql_1 = require("graphql");
function validateFieldConfig(fieldConfig, typeName, fieldName, typeDefs, schema) {
return new Validator(fieldConfig, typeName, fieldName, typeDefs, schema).getValidationInfo();
}
exports.validateFieldConfig = validateFieldConfig;
class Validator {
constructor(fieldConfig, typeName, fieldName, typeDefs, schema) {
var _a;
this.fieldConfig = fieldConfig;
this.typeName = typeName;
this.fieldName = fieldName;
this.typeDefs = typeDefs;
this.schema = schema;
// rebuild schema, as its ast nodes may be undefined
this.schema = graphql_1.buildSchema(graphql_1.printSchema(schema));
try {
this.document = graphql_1.parse(`{${fieldConfig}}`);
}
catch (e) {
throw this.error(e);
}
this.isUnbatched = false;
this.document = graphql_1.visit(this.document, {
Directive: node => node.name.value === 'unbatched'
? (this.isUnbatched = true) && null
: undefined,
});
if (this.isUnbatched)
this.warn('Use of unbatched queries is not recommended as it results in the n+1 problem.');
try {
this.typeDefsDocument = graphql_1.parse(this.typeDefs);
}
catch (e) {
throw this.error(`typeDefs is invalid: ${e}`);
}
const operationDefinition = this.document.definitions[0];
if ((operationDefinition === null || operationDefinition === void 0 ? void 0 : operationDefinition.kind) !== graphql_1.Kind.OPERATION_DEFINITION)
throw this.error('Unable to find operation definition for query.');
if (operationDefinition.selectionSet.selections.length > 1)
throw this.error('Multiple queries or fragments are not allowed.');
this.operationDefinition = operationDefinition;
const queryFieldNode = operationDefinition.selectionSet.selections[0];
if ((queryFieldNode === null || queryFieldNode === void 0 ? void 0 : queryFieldNode.kind) !== graphql_1.Kind.FIELD)
throw this.error('Query type must be a field node.');
this.queryFieldNode = queryFieldNode;
const typeNode = (_a = this.schema.getType(typeName)) === null || _a === void 0 ? void 0 : _a.astNode;
if ((typeNode === null || typeNode === void 0 ? void 0 : typeNode.kind) !== graphql_1.Kind.OBJECT_TYPE_DEFINITION)
throw this.error(`Type "${typeName}" must be an object type.`);
this.typeNode = typeNode;
this.customParameters = this.validateCustomParameters();
this.validateArguments();
this.childType = this.validateReturnType();
this.validateSelections();
}
getValidationInfo() {
return { queryFieldNode: this.queryFieldNode, isUnbatched: this.isUnbatched };
}
validateCustomParameters() {
const customParameters = [];
graphql_1.visit(this.typeDefsDocument, {
ObjectTypeDefinition: node => node.name.value === this.typeName ? node : false,
ObjectTypeExtension: node => node.name.value === this.typeName ? node : false,
FieldDefinition: node => node.name.value === this.fieldName ? node : false,
InputValueDefinition: node => {
customParameters.push(node);
},
});
const variableNames = new Set();
graphql_1.visit(this.operationDefinition, {
Variable: node => {
variableNames.add(node.name.value);
},
});
customParameters.forEach(node => {
var _a;
if (node.type.kind !== graphql_1.Kind.NON_NULL_TYPE)
throw this.error(`All custom parameters must be non-nullable, but $${node.name.value} is of type "${graphql_1.print(node.type)}".`);
if (!variableNames.has(node.name.value))
throw this.error(`Custom parameter $${node.name.value} is not used in query.`);
if ((_a = this.typeNode.fields) === null || _a === void 0 ? void 0 : _a.find(field => field.name.value === node.name.value))
this.warn(`Custom parameter $${node.name.value} eclipses field of the same name in the parent type. ` +
`"${this.typeName}.${node.name.value}" will not be available to use as a variable in the query.`);
});
return customParameters;
}
validateArguments() {
const variableNames = new Set();
graphql_1.visit(this.operationDefinition, {
Variable: node => {
variableNames.add(node.name.value);
},
});
const variableDefinitions = Array.from(variableNames)
.map(variableName => {
var _a;
if (this.customParameters.find(node => node.name.value === variableName))
return;
const fieldNode = (_a = this.typeNode.fields) === null || _a === void 0 ? void 0 : _a.find(field => field.name.value === variableName);
if (!fieldNode)
throw this.error(`Field corresponding to "$${variableName}" not found in type "${this.typeNode.name.value}".`);
return {
kind: graphql_1.Kind.VARIABLE_DEFINITION,
variable: {
kind: graphql_1.Kind.VARIABLE,
name: {
kind: graphql_1.Kind.NAME,
value: variableName,
},
},
type: this.isUnbatched
? fieldNode.type
: {
kind: graphql_1.Kind.NON_NULL_TYPE,
type: {
kind: graphql_1.Kind.LIST_TYPE,
type: {
kind: graphql_1.Kind.NON_NULL_TYPE,
type: {
kind: graphql_1.Kind.NAMED_TYPE,
name: {
kind: graphql_1.Kind.NAME,
value: unwrapTypeNode(fieldNode.type).name.value,
},
},
},
},
},
};
})
.concat(this.customParameters.map(node => ({
kind: graphql_1.Kind.VARIABLE_DEFINITION,
variable: {
kind: graphql_1.Kind.VARIABLE,
name: {
kind: graphql_1.Kind.NAME,
value: node.name.value,
},
},
type: node.type,
})));
const errors = graphql_1.validate(this.schema, graphql_1.visit(this.document, {
OperationDefinition: node => ({ ...node, variableDefinitions }),
}), graphql_1.specifiedRules.filter(rule => this.isUnbatched ? rule !== graphql_1.ScalarLeafsRule : true));
if (errors.length > 0)
throw this.error(errors[0].message);
}
validateReturnType() {
var _a, _b;
const returnType = (_b = (_a = this.schema.getQueryType()) === null || _a === void 0 ? void 0 : _a.getFields()[this.queryFieldNode.name.value]) === null || _b === void 0 ? void 0 : _b.type;
if (!returnType)
throw this.error('Could not find return type for query.');
let intendedType;
graphql_1.visit(this.typeDefsDocument, {
ObjectTypeDefinition: node => node.name.value === this.typeName ? node : false,
ObjectTypeExtension: node => node.name.value === this.typeName ? node : false,
FieldDefinition: node => node.name.value === this.fieldName
? (intendedType = node.type)
: undefined,
});
if (!intendedType)
throw this.error(`Field "${this.typeName}.${this.fieldName}" not found in typeDefs.`);
if (this.isUnbatched) {
const intendedNamedType = graphql_1.typeFromAST(this.schema, intendedType);
if (!intendedNamedType ||
!graphql_1.isTypeSubTypeOf(this.schema, returnType, intendedNamedType)) {
throw this.error(`Query does not return the intended type "${graphql_1.print(intendedType)}" for "${this.typeName}.${this.fieldName}". Returns "${returnType}".`);
}
return null;
}
else {
if (unwrapTypeNode(intendedType).name.value !== unwrapType(returnType).name)
throw this.error(`Query does not return the intended entity type "${unwrapTypeNode(intendedType).name.value}" for "${this.typeName}.${this.fieldName}". Returns "${returnType}".`);
const unwrappedReturnType = unwrapType(returnType);
if (!graphql_1.isListType(graphql_1.isNonNullType(returnType) ? returnType.ofType : returnType) ||
!graphql_1.isObjectType(unwrappedReturnType))
throw this.error(`Query must return a list of objects but instead returns "${returnType}".`);
return unwrappedReturnType;
}
}
validateSelections() {
var _a;
const selections = (_a = this.queryFieldNode.selectionSet) === null || _a === void 0 ? void 0 : _a.selections;
if (this.isUnbatched) {
if (selections === null || selections === void 0 ? void 0 : selections.length)
throw this.error('Selection sets for unbatched queries are unnecessary.');
return;
}
if (!selections)
throw this.error('Query must have a selection set.');
selections.forEach(selection => {
var _a, _b, _c;
if (selection.kind !== graphql_1.Kind.FIELD)
throw this.error('Fragments are not allowed in query.');
const parentFieldName = ((_a = selection.alias) === null || _a === void 0 ? void 0 : _a.value) || selection.name.value;
const parentFieldNode = (_b = this.typeNode.fields) === null || _b === void 0 ? void 0 : _b.find(field => field.name.value === parentFieldName);
if (!parentFieldNode)
throw this.error(`Field corresponding to "${parentFieldName}" in selection set not found in type "${this.typeNode.name.value}". ${selection.alias
? 'Make sure the alias is correctly spelled.'
: 'Use an alias to map the child field to the corresponding parent field.'}`);
if (parentFieldNode.type.kind === graphql_1.Kind.LIST_TYPE && selections.length > 1)
throw this.error(`Only one selection field is allowed when joining on a list type like "${this.typeNode.name.value}.${parentFieldName}".`);
const childFieldName = selection.name.value;
if (!this.childType)
throw this.error('Cannot find the intended type in the schema.');
const childFieldType = (_c = this.childType.getFields()[childFieldName]) === null || _c === void 0 ? void 0 : _c.type;
if (!childFieldType)
throw this.error(`Could not find type definition for "${this.childType.name}.${childFieldName}".`);
if (graphql_1.isListType(childFieldType) && selections.length > 1)
throw this.error(`Only one selection field is allowed when joining on a list type like "${this.childType.name}.${childFieldName}".`);
if (!graphql_1.isScalarType(unwrapType(childFieldType)))
throw this.error(`Cannot join on key "${this.childType.name}.${childFieldName}". Join keys must be scalars or scalar lists.`);
if (unwrapType(childFieldType).name !==
unwrapTypeNode(parentFieldNode.type).name.value)
throw this.error(`Cannot join on keys "${this.typeNode.name.value}.${parentFieldName}" and "${this.childType.name}.${childFieldName}". They are different types: "${unwrapTypeNode(parentFieldNode.type).name.value}" and "${unwrapType(childFieldType).name}".`);
});
}
warn(message) {
console.warn(`graphql-join warning for resolver "${this.typeName}.${this.fieldName}": ${message}`);
}
error(message) {
return new Error(`graphql-join config error for resolver "${this.typeName}.${this.fieldName}": ${message}`);
}
}
function unwrapTypeNode(type) {
return type.kind === graphql_1.Kind.LIST_TYPE || type.kind === graphql_1.Kind.NON_NULL_TYPE
? unwrapTypeNode(type.type)
: type;
}
function unwrapType(type) {
return type && graphql_1.isWrappingType(type) ? unwrapType(type.ofType) : type;
}
//# sourceMappingURL=validate.js.map