UNPKG

graphql-join

Version:

Join types together in your schema declaratively with SDL.

235 lines 12.8 kB
"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