UNPKG

24.3 kBJavaScriptView Raw
1"use strict";
2// Generates a schema for graphql-js given a shorthand schema
3var __extends = (this && this.__extends) || (function () {
4 var extendStatics = Object.setPrototypeOf ||
5 ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
6 function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
7 return function (d, b) {
8 extendStatics(d, b);
9 function __() { this.constructor = d; }
10 d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
11 };
12})();
13Object.defineProperty(exports, "__esModule", { value: true });
14// TODO: document each function clearly in the code: what arguments it accepts
15// and what it outputs.
16// TODO: we should refactor this file, rename it to makeExecutableSchema, and move
17// a bunch of utility functions into a separate utitlities folder, one file per function.
18var graphql_1 = require("graphql");
19var values_1 = require("graphql/execution/values");
20var deprecated_decorator_1 = require("deprecated-decorator");
21var mergeDeep_1 = require("./mergeDeep");
22// @schemaDefinition: A GraphQL type schema in shorthand
23// @resolvers: Definitions for resolvers to be merged with schema
24var SchemaError = /** @class */ (function (_super) {
25 __extends(SchemaError, _super);
26 function SchemaError(message) {
27 var _this = _super.call(this, message) || this;
28 _this.message = message;
29 Error.captureStackTrace(_this, _this.constructor);
30 return _this;
31 }
32 return SchemaError;
33}(Error));
34exports.SchemaError = SchemaError;
35// type definitions can be a string or an array of strings.
36function _generateSchema(typeDefinitions, resolveFunctions, logger,
37 // TODO: rename to allowUndefinedInResolve to be consistent
38 allowUndefinedInResolve, resolverValidationOptions, directiveResolvers, parseOptions) {
39 if (typeof resolverValidationOptions !== 'object') {
40 throw new SchemaError('Expected `resolverValidationOptions` to be an object');
41 }
42 if (!typeDefinitions) {
43 throw new SchemaError('Must provide typeDefs');
44 }
45 if (!resolveFunctions) {
46 throw new SchemaError('Must provide resolvers');
47 }
48 var resolvers = Array.isArray(resolveFunctions)
49 ? resolveFunctions
50 .filter(function (resolverObj) { return typeof resolverObj === 'object'; })
51 .reduce(mergeDeep_1.default, {})
52 : resolveFunctions;
53 // TODO: check that typeDefinitions is either string or array of strings
54 var schema = buildSchemaFromTypeDefinitions(typeDefinitions, parseOptions);
55 addResolveFunctionsToSchema(schema, resolvers, resolverValidationOptions);
56 assertResolveFunctionsPresent(schema, resolverValidationOptions);
57 if (!allowUndefinedInResolve) {
58 addCatchUndefinedToSchema(schema);
59 }
60 if (logger) {
61 addErrorLoggingToSchema(schema, logger);
62 }
63 if (directiveResolvers) {
64 attachDirectiveResolvers(schema, directiveResolvers);
65 }
66 return schema;
67}
68function makeExecutableSchema(_a) {
69 var typeDefs = _a.typeDefs, _b = _a.resolvers, resolvers = _b === void 0 ? {} : _b, connectors = _a.connectors, logger = _a.logger, _c = _a.allowUndefinedInResolve, allowUndefinedInResolve = _c === void 0 ? true : _c, _d = _a.resolverValidationOptions, resolverValidationOptions = _d === void 0 ? {} : _d, _e = _a.directiveResolvers, directiveResolvers = _e === void 0 ? null : _e, _f = _a.parseOptions, parseOptions = _f === void 0 ? {} : _f;
70 var jsSchema = _generateSchema(typeDefs, resolvers, logger, allowUndefinedInResolve, resolverValidationOptions, directiveResolvers, parseOptions);
71 if (typeof resolvers['__schema'] === 'function') {
72 // TODO a bit of a hack now, better rewrite generateSchema to attach it there.
73 // not doing that now, because I'd have to rewrite a lot of tests.
74 addSchemaLevelResolveFunction(jsSchema, resolvers['__schema']);
75 }
76 if (connectors) {
77 // connectors are optional, at least for now. That means you can just import them in the resolve
78 // function if you want.
79 attachConnectorsToContext(jsSchema, connectors);
80 }
81 return jsSchema;
82}
83exports.makeExecutableSchema = makeExecutableSchema;
84function isDocumentNode(typeDefinitions) {
85 return typeDefinitions.kind !== undefined;
86}
87function uniq(array) {
88 return array.reduce(function (accumulator, currentValue) {
89 return accumulator.indexOf(currentValue) === -1
90 ? accumulator.concat([currentValue]) : accumulator;
91 }, []);
92}
93function concatenateTypeDefs(typeDefinitionsAry, calledFunctionRefs) {
94 if (calledFunctionRefs === void 0) { calledFunctionRefs = []; }
95 var resolvedTypeDefinitions = [];
96 typeDefinitionsAry.forEach(function (typeDef) {
97 if (isDocumentNode(typeDef)) {
98 typeDef = graphql_1.print(typeDef);
99 }
100 if (typeof typeDef === 'function') {
101 if (calledFunctionRefs.indexOf(typeDef) === -1) {
102 calledFunctionRefs.push(typeDef);
103 resolvedTypeDefinitions = resolvedTypeDefinitions.concat(concatenateTypeDefs(typeDef(), calledFunctionRefs));
104 }
105 }
106 else if (typeof typeDef === 'string') {
107 resolvedTypeDefinitions.push(typeDef.trim());
108 }
109 else {
110 var type = typeof typeDef;
111 throw new SchemaError("typeDef array must contain only strings and functions, got " + type);
112 }
113 });
114 return uniq(resolvedTypeDefinitions.map(function (x) { return x.trim(); })).join('\n');
115}
116exports.concatenateTypeDefs = concatenateTypeDefs;
117function buildSchemaFromTypeDefinitions(typeDefinitions, parseOptions) {
118 // TODO: accept only array here, otherwise interfaces get confusing.
119 var myDefinitions = typeDefinitions;
120 var astDocument;
121 if (isDocumentNode(typeDefinitions)) {
122 astDocument = typeDefinitions;
123 }
124 else if (typeof myDefinitions !== 'string') {
125 if (!Array.isArray(myDefinitions)) {
126 var type = typeof myDefinitions;
127 throw new SchemaError("typeDefs must be a string, array or schema AST, got " + type);
128 }
129 myDefinitions = concatenateTypeDefs(myDefinitions);
130 }
131 if (typeof myDefinitions === 'string') {
132 astDocument = graphql_1.parse(myDefinitions, parseOptions);
133 }
134 var backcompatOptions = { commentDescriptions: true };
135 // TODO fix types https://github.com/apollographql/graphql-tools/issues/542
136 var schema = graphql_1.buildASTSchema(astDocument, backcompatOptions);
137 var extensionsAst = extractExtensionDefinitions(astDocument);
138 if (extensionsAst.definitions.length > 0) {
139 // TODO fix types https://github.com/apollographql/graphql-tools/issues/542
140 schema = graphql_1.extendSchema(schema, extensionsAst, backcompatOptions);
141 }
142 return schema;
143}
144exports.buildSchemaFromTypeDefinitions = buildSchemaFromTypeDefinitions;
145// This was changed in graphql@0.12
146// See https://github.com/apollographql/graphql-tools/pull/541
147// TODO fix types https://github.com/apollographql/graphql-tools/issues/542
148var oldTypeExtensionDefinitionKind = 'TypeExtensionDefinition';
149var newExtensionDefinitionKind = 'ObjectTypeExtension';
150function extractExtensionDefinitions(ast) {
151 var extensionDefs = ast.definitions.filter(function (def) {
152 return def.kind === oldTypeExtensionDefinitionKind ||
153 def.kind === newExtensionDefinitionKind;
154 });
155 return Object.assign({}, ast, {
156 definitions: extensionDefs,
157 });
158}
159exports.extractExtensionDefinitions = extractExtensionDefinitions;
160function forEachField(schema, fn) {
161 var typeMap = schema.getTypeMap();
162 Object.keys(typeMap).forEach(function (typeName) {
163 var type = typeMap[typeName];
164 // TODO: maybe have an option to include these?
165 if (!graphql_1.getNamedType(type).name.startsWith('__') &&
166 type instanceof graphql_1.GraphQLObjectType) {
167 var fields_1 = type.getFields();
168 Object.keys(fields_1).forEach(function (fieldName) {
169 var field = fields_1[fieldName];
170 fn(field, typeName, fieldName);
171 });
172 }
173 });
174}
175exports.forEachField = forEachField;
176// takes a GraphQL-JS schema and an object of connectors, then attaches
177// the connectors to the context by wrapping each query or mutation resolve
178// function with a function that attaches connectors if they don't exist.
179// attaches connectors only once to make sure they are singletons
180var attachConnectorsToContext = deprecated_decorator_1.deprecated({
181 version: '0.7.0',
182 url: 'https://github.com/apollostack/graphql-tools/issues/140',
183}, function (schema, connectors) {
184 if (!schema || !(schema instanceof graphql_1.GraphQLSchema)) {
185 throw new Error('schema must be an instance of GraphQLSchema. ' +
186 'This error could be caused by installing more than one version of GraphQL-JS');
187 }
188 if (typeof connectors !== 'object') {
189 var connectorType = typeof connectors;
190 throw new Error("Expected connectors to be of type object, got " + connectorType);
191 }
192 if (Object.keys(connectors).length === 0) {
193 throw new Error('Expected connectors to not be an empty object');
194 }
195 if (Array.isArray(connectors)) {
196 throw new Error('Expected connectors to be of type object, got Array');
197 }
198 if (schema['_apolloConnectorsAttached']) {
199 throw new Error('Connectors already attached to context, cannot attach more than once');
200 }
201 schema['_apolloConnectorsAttached'] = true;
202 var attachconnectorFn = function (root, args, ctx) {
203 if (typeof ctx !== 'object') {
204 // if in any way possible, we should throw an error when the attachconnectors
205 // function is called, not when a query is executed.
206 var contextType = typeof ctx;
207 throw new Error("Cannot attach connector because context is not an object: " + contextType);
208 }
209 if (typeof ctx.connectors === 'undefined') {
210 ctx.connectors = {};
211 }
212 Object.keys(connectors).forEach(function (connectorName) {
213 var connector = connectors[connectorName];
214 if (!!connector.prototype) {
215 ctx.connectors[connectorName] = new connector(ctx);
216 }
217 else {
218 throw new Error("Connector must be a function or an class");
219 }
220 });
221 return root;
222 };
223 addSchemaLevelResolveFunction(schema, attachconnectorFn);
224});
225exports.attachConnectorsToContext = attachConnectorsToContext;
226// wraps all resolve functions of query, mutation or subscription fields
227// with the provided function to simulate a root schema level resolve funciton
228function addSchemaLevelResolveFunction(schema, fn) {
229 // TODO test that schema is a schema, fn is a function
230 var rootTypes = [
231 schema.getQueryType(),
232 schema.getMutationType(),
233 schema.getSubscriptionType(),
234 ].filter(function (x) { return !!x; });
235 rootTypes.forEach(function (type) {
236 // XXX this should run at most once per request to simulate a true root resolver
237 // for graphql-js this is an approximation that works with queries but not mutations
238 var rootResolveFn = runAtMostOncePerRequest(fn);
239 var fields = type.getFields();
240 Object.keys(fields).forEach(function (fieldName) {
241 // XXX if the type is a subscription, a same query AST will be ran multiple times so we
242 // deactivate here the runOnce if it's a subscription. This may not be optimal though...
243 if (type === schema.getSubscriptionType()) {
244 fields[fieldName].resolve = wrapResolver(fields[fieldName].resolve, fn);
245 }
246 else {
247 fields[fieldName].resolve = wrapResolver(fields[fieldName].resolve, rootResolveFn);
248 }
249 });
250 });
251}
252exports.addSchemaLevelResolveFunction = addSchemaLevelResolveFunction;
253function getFieldsForType(type) {
254 if (type instanceof graphql_1.GraphQLObjectType ||
255 type instanceof graphql_1.GraphQLInterfaceType) {
256 return type.getFields();
257 }
258 else {
259 return undefined;
260 }
261}
262function addResolveFunctionsToSchema(schema, resolveFunctions, resolverValidationOptions) {
263 if (resolverValidationOptions === void 0) { resolverValidationOptions = {}; }
264 var _a = resolverValidationOptions.allowResolversNotInSchema, allowResolversNotInSchema = _a === void 0 ? false : _a;
265 Object.keys(resolveFunctions).forEach(function (typeName) {
266 var type = schema.getType(typeName);
267 if (!type && typeName !== '__schema') {
268 if (allowResolversNotInSchema) {
269 return;
270 }
271 throw new SchemaError("\"" + typeName + "\" defined in resolvers, but not in schema");
272 }
273 Object.keys(resolveFunctions[typeName]).forEach(function (fieldName) {
274 if (fieldName.startsWith('__')) {
275 // this is for isTypeOf and resolveType and all the other stuff.
276 // TODO require resolveType for unions and interfaces.
277 type[fieldName.substring(2)] = resolveFunctions[typeName][fieldName];
278 return;
279 }
280 if (type instanceof graphql_1.GraphQLScalarType) {
281 type[fieldName] = resolveFunctions[typeName][fieldName];
282 return;
283 }
284 if (type instanceof graphql_1.GraphQLEnumType) {
285 if (!type.getValue(fieldName)) {
286 throw new SchemaError(typeName + "." + fieldName + " was defined in resolvers, but enum is not in schema");
287 }
288 type.getValue(fieldName)['value'] =
289 resolveFunctions[typeName][fieldName];
290 return;
291 }
292 var fields = getFieldsForType(type);
293 if (!fields) {
294 if (allowResolversNotInSchema) {
295 return;
296 }
297 throw new SchemaError(typeName + " was defined in resolvers, but it's not an object");
298 }
299 if (!fields[fieldName]) {
300 if (allowResolversNotInSchema) {
301 return;
302 }
303 throw new SchemaError(typeName + "." + fieldName + " defined in resolvers, but not in schema");
304 }
305 var field = fields[fieldName];
306 var fieldResolve = resolveFunctions[typeName][fieldName];
307 if (typeof fieldResolve === 'function') {
308 // for convenience. Allows shorter syntax in resolver definition file
309 setFieldProperties(field, { resolve: fieldResolve });
310 }
311 else {
312 if (typeof fieldResolve !== 'object') {
313 throw new SchemaError("Resolver " + typeName + "." + fieldName + " must be object or function");
314 }
315 setFieldProperties(field, fieldResolve);
316 }
317 });
318 });
319}
320exports.addResolveFunctionsToSchema = addResolveFunctionsToSchema;
321function setFieldProperties(field, propertiesObj) {
322 Object.keys(propertiesObj).forEach(function (propertyName) {
323 field[propertyName] = propertiesObj[propertyName];
324 });
325}
326function assertResolveFunctionsPresent(schema, resolverValidationOptions) {
327 if (resolverValidationOptions === void 0) { resolverValidationOptions = {}; }
328 var _a = resolverValidationOptions.requireResolversForArgs, requireResolversForArgs = _a === void 0 ? false : _a, _b = resolverValidationOptions.requireResolversForNonScalar, requireResolversForNonScalar = _b === void 0 ? false : _b, _c = resolverValidationOptions.requireResolversForAllFields, requireResolversForAllFields = _c === void 0 ? false : _c;
329 if (requireResolversForAllFields &&
330 (requireResolversForArgs || requireResolversForNonScalar)) {
331 throw new TypeError('requireResolversForAllFields takes precedence over the more specific assertions. ' +
332 'Please configure either requireResolversForAllFields or requireResolversForArgs / ' +
333 'requireResolversForNonScalar, but not a combination of them.');
334 }
335 forEachField(schema, function (field, typeName, fieldName) {
336 // requires a resolve function for *every* field.
337 if (requireResolversForAllFields) {
338 expectResolveFunction(field, typeName, fieldName);
339 }
340 // requires a resolve function on every field that has arguments
341 if (requireResolversForArgs && field.args.length > 0) {
342 expectResolveFunction(field, typeName, fieldName);
343 }
344 // requires a resolve function on every field that returns a non-scalar type
345 if (requireResolversForNonScalar &&
346 !(graphql_1.getNamedType(field.type) instanceof graphql_1.GraphQLScalarType)) {
347 expectResolveFunction(field, typeName, fieldName);
348 }
349 });
350}
351exports.assertResolveFunctionsPresent = assertResolveFunctionsPresent;
352function expectResolveFunction(field, typeName, fieldName) {
353 if (!field.resolve) {
354 console.warn(
355 // tslint:disable-next-line: max-line-length
356 "Resolve function missing for \"" + typeName + "." + fieldName + "\". To disable this warning check https://github.com/apollostack/graphql-tools/issues/131");
357 return;
358 }
359 if (typeof field.resolve !== 'function') {
360 throw new SchemaError("Resolver \"" + typeName + "." + fieldName + "\" must be a function");
361 }
362}
363function addErrorLoggingToSchema(schema, logger) {
364 if (!logger) {
365 throw new Error('Must provide a logger');
366 }
367 if (typeof logger.log !== 'function') {
368 throw new Error('Logger.log must be a function');
369 }
370 forEachField(schema, function (field, typeName, fieldName) {
371 var errorHint = typeName + "." + fieldName;
372 field.resolve = decorateWithLogger(field.resolve, logger, errorHint);
373 });
374}
375exports.addErrorLoggingToSchema = addErrorLoggingToSchema;
376// XXX badly named function. this doesn't really wrap, it just chains resolvers...
377function wrapResolver(innerResolver, outerResolver) {
378 return function (obj, args, ctx, info) {
379 return Promise.resolve(outerResolver(obj, args, ctx, info)).then(function (root) {
380 if (innerResolver) {
381 return innerResolver(root, args, ctx, info);
382 }
383 return graphql_1.defaultFieldResolver(root, args, ctx, info);
384 });
385 };
386}
387function chainResolvers(resolvers) {
388 return function (root, args, ctx, info) {
389 return resolvers.reduce(function (prev, curResolver) {
390 if (curResolver) {
391 return curResolver(prev, args, ctx, info);
392 }
393 return graphql_1.defaultFieldResolver(prev, args, ctx, info);
394 }, root);
395 };
396}
397exports.chainResolvers = chainResolvers;
398/*
399 * fn: The function to decorate with the logger
400 * logger: an object instance of type Logger
401 * hint: an optional hint to add to the error's message
402 */
403function decorateWithLogger(fn, logger, hint) {
404 if (typeof fn === 'undefined') {
405 fn = graphql_1.defaultFieldResolver;
406 }
407 var logError = function (e) {
408 // TODO: clone the error properly
409 var newE = new Error();
410 newE.stack = e.stack;
411 /* istanbul ignore else: always get the hint from addErrorLoggingToSchema */
412 if (hint) {
413 newE['originalMessage'] = e.message;
414 newE['message'] = "Error in resolver " + hint + "\n" + e.message;
415 }
416 logger.log(newE);
417 };
418 return function (root, args, ctx, info) {
419 try {
420 var result = fn(root, args, ctx, info);
421 // If the resolve function returns a Promise log any Promise rejects.
422 if (result &&
423 typeof result.then === 'function' &&
424 typeof result.catch === 'function') {
425 result.catch(function (reason) {
426 // make sure that it's an error we're logging.
427 var error = reason instanceof Error ? reason : new Error(reason);
428 logError(error);
429 // We don't want to leave an unhandled exception so pass on error.
430 return reason;
431 });
432 }
433 return result;
434 }
435 catch (e) {
436 logError(e);
437 // we want to pass on the error, just in case.
438 throw e;
439 }
440 };
441}
442function addCatchUndefinedToSchema(schema) {
443 forEachField(schema, function (field, typeName, fieldName) {
444 var errorHint = typeName + "." + fieldName;
445 field.resolve = decorateToCatchUndefined(field.resolve, errorHint);
446 });
447}
448exports.addCatchUndefinedToSchema = addCatchUndefinedToSchema;
449function decorateToCatchUndefined(fn, hint) {
450 if (typeof fn === 'undefined') {
451 fn = graphql_1.defaultFieldResolver;
452 }
453 return function (root, args, ctx, info) {
454 var result = fn(root, args, ctx, info);
455 if (typeof result === 'undefined') {
456 throw new Error("Resolve function for \"" + hint + "\" returned undefined");
457 }
458 return result;
459 };
460}
461// XXX this function only works for resolvers
462// XXX very hacky way to remember if the function
463// already ran for this request. This will only work
464// if people don't actually cache the operation.
465// if they do cache the operation, they will have to
466// manually remove the __runAtMostOnce before every request.
467function runAtMostOncePerRequest(fn) {
468 var value;
469 var randomNumber = Math.random();
470 return function (root, args, ctx, info) {
471 if (!info.operation['__runAtMostOnce']) {
472 info.operation['__runAtMostOnce'] = {};
473 }
474 if (!info.operation['__runAtMostOnce'][randomNumber]) {
475 info.operation['__runAtMostOnce'][randomNumber] = true;
476 value = fn(root, args, ctx, info);
477 }
478 return value;
479 };
480}
481function attachDirectiveResolvers(schema, directiveResolvers) {
482 if (typeof directiveResolvers !== 'object') {
483 throw new Error("Expected directiveResolvers to be of type object, got " + typeof directiveResolvers);
484 }
485 if (Array.isArray(directiveResolvers)) {
486 throw new Error('Expected directiveResolvers to be of type object, got Array');
487 }
488 forEachField(schema, function (field) {
489 var directives = field.astNode.directives;
490 directives.forEach(function (directive) {
491 var directiveName = directive.name.value;
492 var resolver = directiveResolvers[directiveName];
493 if (resolver) {
494 var originalResolver_1 = field.resolve || graphql_1.defaultFieldResolver;
495 var Directive = schema.getDirective(directiveName);
496 if (typeof Directive === 'undefined') {
497 throw new Error("Directive @" + directiveName + " is undefined. " +
498 'Please define in schema before using');
499 }
500 var directiveArgs_1 = values_1.getArgumentValues(Directive, directive);
501 field.resolve = function () {
502 var args = [];
503 for (var _i = 0; _i < arguments.length; _i++) {
504 args[_i] = arguments[_i];
505 }
506 var source = args[0], context = args[2], info = args[3];
507 return resolver(function () {
508 try {
509 var promise = originalResolver_1.call.apply(originalResolver_1, [field].concat(args));
510 if (promise instanceof Promise) {
511 return promise;
512 }
513 return Promise.resolve(promise);
514 }
515 catch (error) {
516 return Promise.reject(error);
517 }
518 }, source, directiveArgs_1, context, info);
519 };
520 }
521 });
522 });
523}
524exports.attachDirectiveResolvers = attachDirectiveResolvers;
525//# sourceMappingURL=schemaGenerator.js.map
\No newline at end of file