UNPKG

graphql-tools

Version:
406 lines (358 loc) • 16.2 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.attachConnectorsToContext = exports.addSchemaLevelResolveFunction = exports.buildSchemaFromTypeDefinitions = exports.assertResolveFunctionsPresent = exports.addCatchUndefinedToSchema = exports.addResolveFunctionsToSchema = exports.addErrorLoggingToSchema = exports.forEachField = exports.SchemaError = exports.makeExecutableSchema = exports.generateSchema = undefined; var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; }; // Generates a schema for graphql-js given a shorthand schema // TODO: document each function clearly in the code: what arguments it accepts // and what it outputs. var _language = require('graphql/language'); var _lodash = require('lodash.uniq'); var _lodash2 = _interopRequireDefault(_lodash); var _utilities = require('graphql/utilities'); var _type = require('graphql/type'); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } // @schemaDefinition: A GraphQL type schema in shorthand // @resolvers: Definitions for resolvers to be merged with schema function SchemaError(message) { Error.captureStackTrace(this, this.constructor); this.message = message; } SchemaError.prototype = new Error(); function generateSchema() { console.error('generateSchema is deprecated, use makeExecutableSchema instead'); return _generateSchema.apply(undefined, arguments); } // type definitions can be a string or an array of strings. function _generateSchema(typeDefinitions, resolveFunctions, logger) { var allowUndefinedInResolve = arguments.length <= 3 || arguments[3] === undefined ? true : arguments[3]; var resolverValidationOptions = arguments.length <= 4 || arguments[4] === undefined ? {} : arguments[4]; if ((typeof resolverValidationOptions === 'undefined' ? 'undefined' : _typeof(resolverValidationOptions)) !== 'object') { throw new SchemaError('Expected `resolverValidationOptions` to be an object'); } if (!typeDefinitions) { throw new SchemaError('Must provide typeDefinitions'); } if (!resolveFunctions) { throw new SchemaError('Must provide resolveFunctions'); } // TODO: check that typeDefinitions is either string or array of strings var schema = buildSchemaFromTypeDefinitions(typeDefinitions); addResolveFunctionsToSchema(schema, resolveFunctions); assertResolveFunctionsPresent(schema, resolverValidationOptions); if (!allowUndefinedInResolve) { addCatchUndefinedToSchema(schema); } if (logger) { addErrorLoggingToSchema(schema, logger); } return schema; } function makeExecutableSchema(_ref) { var typeDefs = _ref.typeDefs; var resolvers = _ref.resolvers; var connectors = _ref.connectors; var logger = _ref.logger; var _ref$allowUndefinedIn = _ref.allowUndefinedInResolve; var allowUndefinedInResolve = _ref$allowUndefinedIn === undefined ? false : _ref$allowUndefinedIn; var _ref$resolverValidati = _ref.resolverValidationOptions; var resolverValidationOptions = _ref$resolverValidati === undefined ? {} : _ref$resolverValidati; var jsSchema = _generateSchema(typeDefs, resolvers, logger, allowUndefinedInResolve, resolverValidationOptions); if (typeof resolvers.__schema === 'function') { // TODO a bit of a hack now, better rewrite generateSchema to attach it there. // not doing that now, because I'd have to rewrite a lot of tests. addSchemaLevelResolveFunction(jsSchema, resolvers.__schema); } if (connectors) { // connectors are optional, at least for now. That means you can just import them in the resolve // function if you want. attachConnectorsToContext(jsSchema, connectors); } return jsSchema; } function concatenateTypeDefs(typeDefinitionsAry) { var functionsCalled = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; var resolvedTypeDefinitions = []; typeDefinitionsAry.forEach(function (typeDef) { if (typeof typeDef === 'function') { if (!(typeDef in functionsCalled)) { // eslint-disable-next-line no-param-reassign functionsCalled[typeDef] = 1; resolvedTypeDefinitions = resolvedTypeDefinitions.concat(concatenateTypeDefs(typeDef(), functionsCalled)); } } else { if (typeof typeDef === 'string') { resolvedTypeDefinitions.push(typeDef.trim()); } else { var type = typeof typeDef === 'undefined' ? 'undefined' : _typeof(typeDef); throw new SchemaError('typeDef array must contain only strings and functions, got ' + type); } } }); return (0, _lodash2.default)(resolvedTypeDefinitions.map(function (x) { return x.trim(); })).join('\n'); } function buildSchemaFromTypeDefinitions(typeDefinitions) { // TODO: accept only array here, otherwise interfaces get confusing. var myDefinitions = typeDefinitions; if (typeof myDefinitions !== 'string') { if (!Array.isArray(myDefinitions)) { // TODO improve error message and say what type was actually found throw new SchemaError('`typeDefinitions` must be a string or array'); } myDefinitions = concatenateTypeDefs(myDefinitions); } return (0, _utilities.buildASTSchema)((0, _language.parse)(myDefinitions)); } function forEachField(schema, fn) { var typeMap = schema.getTypeMap(); Object.keys(typeMap).forEach(function (typeName) { var type = typeMap[typeName]; // TODO: maybe have an option to include these? if (!(0, _type.getNamedType)(type).name.startsWith('__') && type instanceof _type.GraphQLObjectType) { (function () { var fields = type.getFields(); Object.keys(fields).forEach(function (fieldName) { var field = fields[fieldName]; fn(field, typeName, fieldName); }); })(); } }); } // takes a GraphQL-JS schema and an object of connectors, then attaches // the connectors to the context by wrapping each query or mutation resolve // function with a function that attaches connectors if they don't exist. // attaches connectors only once to make sure they are singletons function attachConnectorsToContext(schema, connectors) { if (!schema || !(schema instanceof _type.GraphQLSchema)) { throw new Error('schema must be an instance of GraphQLSchema. ' + 'This error could be caused by installing more than one version of GraphQL-JS'); } if ((typeof connectors === 'undefined' ? 'undefined' : _typeof(connectors)) !== 'object') { var connectorType = typeof connectors === 'undefined' ? 'undefined' : _typeof(connectors); throw new Error('Expected connectors to be of type object, got ' + connectorType); } if (Object.keys(connectors).length === 0) { throw new Error('Expected connectors to not be an empty object'); } if (Array.isArray(connectors)) { throw new Error('Expected connectors to be of type object, got Array'); } if (schema._apolloConnectorsAttached) { throw new Error('Connectors already attached to context, cannot attach more than once'); } // eslint-disable-next-line no-param-reassign schema._apolloConnectorsAttached = true; var attachconnectorFn = function attachconnectorFn(root, args, ctx) { if ((typeof ctx === 'undefined' ? 'undefined' : _typeof(ctx)) !== 'object') { // if in any way possible, we should throw an error when the attachconnectors // function is called, not when a query is executed. var contextType = typeof ctx === 'undefined' ? 'undefined' : _typeof(ctx); throw new Error('Cannot attach connector because context is not an object: ' + contextType); } if (typeof ctx.connectors === 'undefined') { // eslint-disable-next-line no-param-reassign ctx.connectors = {}; } Object.keys(connectors).forEach(function (connectorName) { // eslint-disable-next-line no-param-reassign ctx.connectors[connectorName] = new connectors[connectorName](ctx); }); return root; }; addSchemaLevelResolveFunction(schema, attachconnectorFn); } // wraps all resolve functions of query, mutation or subscription fields // with the provided function to simulate a root schema level resolve funciton function addSchemaLevelResolveFunction(schema, fn) { // TODO test that schema is a schema, fn is a function var rootTypes = [schema.getQueryType(), schema.getMutationType(), schema.getSubscriptionType()].filter(function (x) { return !!x; }); // XXX this should run at most once per request to simulate a true root resolver // for graphql-js this is an approximation that works with queries but not mutations var rootResolveFn = runAtMostOncePerTick(fn); rootTypes.forEach(function (type) { var fields = type.getFields(); Object.keys(fields).forEach(function (fieldName) { fields[fieldName].resolve = wrapResolver(fields[fieldName].resolve, rootResolveFn); }); }); } function addResolveFunctionsToSchema(schema, resolveFunctions) { Object.keys(resolveFunctions).forEach(function (typeName) { var type = schema.getType(typeName); if (!type && typeName !== '__schema') { throw new SchemaError('"' + typeName + '" defined in resolvers, but not in schema'); } Object.keys(resolveFunctions[typeName]).forEach(function (fieldName) { if (fieldName.startsWith('__')) { // this is for isTypeOf and resolveType and all the other stuff. // TODO require resolveType for unions and interfaces. type[fieldName.substring(2)] = resolveFunctions[typeName][fieldName]; return; } if (!type.getFields()[fieldName]) { throw new SchemaError(typeName + '.' + fieldName + ' defined in resolvers, but not in schema'); } var field = type.getFields()[fieldName]; var fieldResolve = resolveFunctions[typeName][fieldName]; if (typeof fieldResolve === 'function') { // for convenience. Allows shorter syntax in resolver definition file setFieldProperties(field, { resolve: fieldResolve }); } else { if ((typeof fieldResolve === 'undefined' ? 'undefined' : _typeof(fieldResolve)) !== 'object') { throw new SchemaError('Resolver ' + typeName + '.' + fieldName + ' must be object or function'); } setFieldProperties(field, fieldResolve); } }); }); } function setFieldProperties(field, propertiesObj) { Object.keys(propertiesObj).forEach(function (propertyName) { // eslint-disable-next-line no-param-reassign field[propertyName] = propertiesObj[propertyName]; }); } function assertResolveFunctionsPresent(schema) { var resolverValidationOptions = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; var _resolverValidationOp = resolverValidationOptions.requireResolversForArgs; var requireResolversForArgs = _resolverValidationOp === undefined ? true : _resolverValidationOp; var _resolverValidationOp2 = resolverValidationOptions.requireResolversForNonScalar; var requireResolversForNonScalar = _resolverValidationOp2 === undefined ? true : _resolverValidationOp2; forEachField(schema, function (field, typeName, fieldName) { // requires a resolve function on every field that has arguments if (requireResolversForArgs && field.args.length > 0) { expectResolveFunction(field, typeName, fieldName); } // requires a resolve function on every field that returns a non-scalar type if (requireResolversForNonScalar && !((0, _type.getNamedType)(field.type) instanceof _type.GraphQLScalarType)) { expectResolveFunction(field, typeName, fieldName); } }); } function expectResolveFunction(field, typeName, fieldName) { if (!field.resolve) { throw new SchemaError('Resolve function missing for "' + typeName + '.' + fieldName + '"'); } if (typeof field.resolve !== 'function') { throw new SchemaError('Resolver "' + typeName + '.' + fieldName + '" must be a function'); } } function addErrorLoggingToSchema(schema, logger) { if (!logger) { throw new Error('Must provide a logger'); } if (typeof logger.log !== 'function') { throw new Error('Logger.log must be a function'); } forEachField(schema, function (field, typeName, fieldName) { var errorHint = typeName + '.' + fieldName; // eslint-disable-next-line no-param-reassign field.resolve = decorateWithLogger(field.resolve, logger, errorHint); }); } function wrapResolver(innerResolver, outerResolver) { return function (obj, args, ctx, info) { var root = outerResolver(obj, args, ctx, info); if (innerResolver) { return innerResolver(root, args, ctx, info); } return defaultResolveFn(root, args, ctx, info); }; } /* * fn: The function to decorate with the logger * logger: an object instance of type Logger * hint: an optional hint to add to the error's message */ function decorateWithLogger(fn, logger) { var hint = arguments.length <= 2 || arguments[2] === undefined ? '' : arguments[2]; if (typeof fn === 'undefined') { // eslint-disable-next-line no-param-reassign fn = defaultResolveFn; } return function () { try { return fn.apply(undefined, arguments); } catch (e) { // TODO: clone the error properly var newE = new Error(); newE.stack = e.stack; if (hint) { newE.originalMessage = e.message; newE.message = 'Error in resolver ' + hint + '\n' + e.message; } logger.log(newE); // we want to pass on the error, just in case. throw e; } }; } function addCatchUndefinedToSchema(schema) { forEachField(schema, function (field, typeName, fieldName) { var errorHint = typeName + '.' + fieldName; // eslint-disable-next-line no-param-reassign field.resolve = decorateToCatchUndefined(field.resolve, errorHint); }); } function decorateToCatchUndefined(fn, hint) { if (typeof fn === 'undefined') { // eslint-disable-next-line no-param-reassign fn = defaultResolveFn; } return function () { var result = fn.apply(undefined, arguments); if (typeof result === 'undefined') { throw new Error('Resolve function for "' + hint + '" returned undefined'); } return result; }; } // XXX this function needs a shim to work in the browser function runAtMostOncePerTick(fn) { var count = 0; var value = void 0; return function () { if (count === 0) { value = fn.apply(undefined, arguments); count += 1; process.nextTick(function () { count = 0; }); } return value; }; } /** * XXX taken from graphql-js: src/execution/execute.js, because that function * is not exported * * If a resolve function is not given, then a default resolve behavior is used * which takes the property of the source object of the same name as the field * and returns it as the result, or if it's a function, returns the result * of calling that function. */ function defaultResolveFn(source, args, context, _ref2) { var fieldName = _ref2.fieldName; // ensure source is a value for which property access is acceptable. if ((typeof source === 'undefined' ? 'undefined' : _typeof(source)) === 'object' || typeof source === 'function') { var property = source[fieldName]; return typeof property === 'function' ? source[fieldName]() : property; } return undefined; } exports.generateSchema = generateSchema; exports. // TODO deprecated, remove for v 0.4.x makeExecutableSchema = makeExecutableSchema; exports.SchemaError = SchemaError; exports.forEachField = forEachField; exports.addErrorLoggingToSchema = addErrorLoggingToSchema; exports.addResolveFunctionsToSchema = addResolveFunctionsToSchema; exports.addCatchUndefinedToSchema = addCatchUndefinedToSchema; exports.assertResolveFunctionsPresent = assertResolveFunctionsPresent; exports.buildSchemaFromTypeDefinitions = buildSchemaFromTypeDefinitions; exports.addSchemaLevelResolveFunction = addSchemaLevelResolveFunction; exports.attachConnectorsToContext = attachConnectorsToContext;