graphql-modules
Version:
Create reusable, maintainable, testable and extendable GraphQL modules
342 lines (341 loc) • 15.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.createResolvers = createResolvers;
exports.readResolverMetadata = readResolverMetadata;
const graphql_1 = require("graphql");
const errors_1 = require("./../shared/errors");
const utils_1 = require("../shared/utils");
const middleware_1 = require("../shared/middleware");
const resolverMetadataProp = Symbol('metadata');
function createResolvers(config, metadata, app) {
const ensure = ensureImplements(metadata);
const normalizedModuleMiddlewareMap = config.middlewares || {};
const middlewareMap = (0, middleware_1.mergeMiddlewareMaps)(app.middlewareMap, normalizedModuleMiddlewareMap);
(0, middleware_1.validateMiddlewareMap)(normalizedModuleMiddlewareMap, metadata);
const resolvers = addDefaultResolvers(mergeResolvers(config), middlewareMap, config);
// Wrap resolvers
for (const typeName in resolvers) {
if (resolvers.hasOwnProperty(typeName)) {
const obj = resolvers[typeName];
if (isScalarResolver(obj)) {
continue;
}
else if (isEnumResolver(obj)) {
continue;
}
else if (obj && typeof obj === 'object') {
for (const fieldName in obj) {
if (obj.hasOwnProperty(fieldName)) {
ensure.type(typeName, fieldName);
const path = [typeName, fieldName];
// function
if (isResolveFn(obj[fieldName])) {
const resolver = wrapResolver({
config,
resolver: obj[fieldName],
middlewareMap,
path,
isTypeResolver: fieldName === '__isTypeOf' || fieldName === '__resolveType',
isReferenceResolver: fieldName === '__resolveReference',
isObjectResolver: fieldName === '__resolveObject',
});
resolvers[typeName][fieldName] = resolver;
}
else if (isResolveOptions(obj[fieldName])) {
// { resolve }
if ((0, utils_1.isDefined)(obj[fieldName].resolve)) {
const resolver = wrapResolver({
config,
resolver: obj[fieldName].resolve,
middlewareMap,
path,
});
resolvers[typeName][fieldName].resolve = resolver;
}
// { subscribe }
if ((0, utils_1.isDefined)(obj[fieldName].subscribe)) {
const resolver = wrapResolver({
config,
resolver: obj[fieldName].subscribe,
middlewareMap,
path,
});
resolvers[typeName][fieldName].subscribe = resolver;
}
if ((0, utils_1.isDefined)(obj[fieldName].extensions)) {
// Do NOT add a resolve if one is not specified, it will cause
// change in behavior in systems like `grafast`
resolvers[typeName][fieldName].extensions =
obj[fieldName].extensions;
}
}
}
}
}
}
}
return resolvers;
}
/**
* Wrap a resolver so we use module's context instead of app context.
* Use a middleware if available.
* Attach metadata to a resolver (we will see if it's helpful, probably in error handling)
*/
function wrapResolver({ resolver, config, path, middlewareMap, isTypeResolver, isReferenceResolver, isObjectResolver, }) {
if (isTypeResolver || isReferenceResolver) {
const wrappedResolver = (root, context, info) => {
const ctx = {
root,
context: isReferenceResolver
? context.ɵgetModuleContext(config.id, context)
: // We mark the context object as possibly undefined,
// because graphql-jit for some reason doesn't pass it for isTypeOf or resolveType methods
context === null || context === void 0 ? void 0 : context.ɵgetModuleContext(config.id, context),
info,
};
return resolver(ctx.root, ctx.context, ctx.info);
};
writeResolverMetadata(wrappedResolver, config);
return wrappedResolver;
}
if (isObjectResolver) {
const wrappedResolver = (root, fields, context, info) => {
const moduleContext = context.ɵgetModuleContext(config.id, context);
return resolver(root, fields, moduleContext, info);
};
writeResolverMetadata(wrappedResolver, config);
return wrappedResolver;
}
const middleware = (0, middleware_1.createMiddleware)(path, middlewareMap);
const wrappedResolver = (root, args, context, info) => {
const ctx = {
root,
args,
context: context.ɵgetModuleContext(config.id, context),
info,
};
return middleware(ctx, () => resolver(ctx.root, ctx.args, ctx.context, ctx.info));
};
writeResolverMetadata(wrappedResolver, config);
return wrappedResolver;
}
/**
* We iterate over every defined resolver and check if it's valid and not duplicated
*/
function mergeResolvers(config) {
if (!config.resolvers) {
return {};
}
const resolvers = Array.isArray(config.resolvers)
? config.resolvers
: [config.resolvers];
const container = {};
for (const currentResolvers of resolvers) {
for (const typeName in currentResolvers) {
if (currentResolvers.hasOwnProperty(typeName)) {
const value = currentResolvers[typeName];
if ((0, utils_1.isNil)(value)) {
continue;
}
else if (isScalarResolver(value)) {
addScalar({ typeName, resolver: value, container, config });
}
else if (isEnumResolver(value)) {
addEnum({ typeName, resolver: value, container, config });
}
else if (value && typeof value === 'object') {
addObject({ typeName, fields: value, container, config });
}
else {
throw new errors_1.ResolverInvalidError(`Resolver of "${typeName}" is invalid`, (0, errors_1.useLocation)({ dirname: config.dirname, id: config.id }));
}
}
}
}
return container;
}
function addObject({ typeName, fields, container, config, }) {
if (!container[typeName]) {
container[typeName] = {};
}
for (const fieldName in fields) {
if (fields.hasOwnProperty(fieldName)) {
const resolver = fields[fieldName];
if (isResolveFn(resolver)) {
if (container[typeName][fieldName]) {
throw new errors_1.ResolverDuplicatedError(`Duplicated resolver of "${typeName}.${fieldName}"`, (0, errors_1.useLocation)({ dirname: config.dirname, id: config.id }));
}
writeResolverMetadata(resolver, config);
container[typeName][fieldName] = resolver;
}
else if (isResolveOptions(resolver)) {
if (!container[typeName][fieldName]) {
container[typeName][fieldName] = {};
}
// resolve
if ((0, utils_1.isDefined)(resolver.resolve)) {
if (container[typeName][fieldName].resolve) {
throw new errors_1.ResolverDuplicatedError(`Duplicated resolver of "${typeName}.${fieldName}" (resolve method)`, (0, errors_1.useLocation)({ dirname: config.dirname, id: config.id }));
}
writeResolverMetadata(resolver.resolve, config);
container[typeName][fieldName].resolve = resolver.resolve;
}
// subscribe
if ((0, utils_1.isDefined)(resolver.subscribe)) {
if (container[typeName][fieldName].subscribe) {
throw new errors_1.ResolverDuplicatedError(`Duplicated resolver of "${typeName}.${fieldName}" (subscribe method)`, (0, errors_1.useLocation)({ dirname: config.dirname, id: config.id }));
}
writeResolverMetadata(resolver.subscribe, config);
container[typeName][fieldName].subscribe = resolver.subscribe;
}
// extensions
if ((0, utils_1.isDefined)(resolver.extensions)) {
if (container[typeName][fieldName].extensions) {
throw new errors_1.ResolverDuplicatedError(`Duplicated resolver of "${typeName}.${fieldName}" (extensions object)`, (0, errors_1.useLocation)({ dirname: config.dirname, id: config.id }));
}
writeResolverMetadata(resolver.extensions, config);
container[typeName][fieldName].extensions = resolver.extensions;
}
}
}
}
}
function addScalar({ typeName, resolver, container, config, }) {
if (container[typeName]) {
throw new errors_1.ResolverDuplicatedError(`Duplicated resolver of scalar "${typeName}"`, (0, errors_1.useLocation)({ dirname: config.dirname, id: config.id }));
}
writeResolverMetadata(resolver.parseLiteral, config);
writeResolverMetadata(resolver.parseValue, config);
writeResolverMetadata(resolver.serialize, config);
container[typeName] = resolver;
}
function addEnum({ typeName, resolver, container, config, }) {
if (!container[typeName]) {
container[typeName] = {};
}
for (const key in resolver) {
if (resolver.hasOwnProperty(key)) {
const value = resolver[key];
if (container[typeName][key]) {
throw new errors_1.ResolverDuplicatedError(`Duplicated resolver of "${typeName}.${key}" enum value`, (0, errors_1.useLocation)({ dirname: config.dirname, id: config.id }));
}
container[typeName][key] = value;
}
}
}
/**
* Helps to make sure a resolver has a corresponding type/field definition.
* We don't want to pass resolve function that are not related to the module.
*/
function ensureImplements(metadata) {
return {
type(name, field) {
var _a, _b;
const type = []
.concat((_a = metadata.implements) === null || _a === void 0 ? void 0 : _a[name], (_b = metadata.extends) === null || _b === void 0 ? void 0 : _b[name])
.filter(utils_1.isDefined);
if (type === null || type === void 0 ? void 0 : type.includes(field)) {
return true;
}
const id = `"${name}.${field}"`;
throw new errors_1.ExtraResolverError(`Resolver of "${id}" type cannot be implemented`, `${id} is not defined`, (0, errors_1.useLocation)({ dirname: metadata.dirname, id: metadata.id }));
},
scalar(name) {
var _a;
if ((((_a = metadata.implements) === null || _a === void 0 ? void 0 : _a.__scalars) || []).includes(name)) {
return true;
}
throw new errors_1.ExtraResolverError(`Resolver of "${name}" scalar cannot be implemented`, `${name} is not defined`, (0, errors_1.useLocation)({ dirname: metadata.dirname, id: metadata.id }));
},
};
}
function writeResolverMetadata(resolver, config) {
if (!resolver) {
return;
}
resolver[resolverMetadataProp] = {
moduleId: config.id,
};
}
function readResolverMetadata(resolver) {
return resolver[resolverMetadataProp];
}
/**
* In order to use middlewares on fields
* that are defined in SDL but have no implemented resolvers,
* we would have to recreate GraphQLSchema and wrap resolve functions.
*
* Since we can't access GraphQLSchema on a module level
* and recreating GraphQLSchema seems unreasonable,
* we can create default resolvers instead.
*
* @example
*
* gql`
* type Query {
* me: User!
* }
*
* type User {
* name: String!
* }
* `
*
* The resolver of `Query.me` is implemented and resolver of `User.name` is not.
* In case where a middleware wants to intercept the resolver of `User.name`,
* we use a default field resolver from `graphql` package
* and put it next to other defined resolvers.
*
* This way our current logic of wrapping resolvers and running
* middleware functions stays untouched.
*/
function addDefaultResolvers(resolvers, middlewareMap, config) {
const container = resolvers;
const sdl = Array.isArray(config.typeDefs)
? (0, graphql_1.concatAST)(config.typeDefs)
: config.typeDefs;
function hasMiddleware(typeName, fieldName) {
var _a, _b, _c, _d, _e, _f;
return ((((_b = (_a = middlewareMap['*']) === null || _a === void 0 ? void 0 : _a['*']) === null || _b === void 0 ? void 0 : _b.length) ||
((_d = (_c = middlewareMap[typeName]) === null || _c === void 0 ? void 0 : _c['*']) === null || _d === void 0 ? void 0 : _d.length) ||
((_f = (_e = middlewareMap[typeName]) === null || _e === void 0 ? void 0 : _e[fieldName]) === null || _f === void 0 ? void 0 : _f.length)) > 0);
}
sdl.definitions.forEach((definition) => {
if (definition.kind === graphql_1.Kind.OBJECT_TYPE_DEFINITION ||
definition.kind === graphql_1.Kind.OBJECT_TYPE_EXTENSION) {
// Right now we only support Object type
if (definition.fields) {
const typeName = definition.name.value;
definition.fields.forEach((field) => {
var _a;
const fieldName = field.name.value;
if (!((_a = container[typeName]) === null || _a === void 0 ? void 0 : _a[fieldName]) &&
hasMiddleware(typeName, fieldName)) {
if (!container[typeName]) {
container[typeName] = {};
}
container[typeName][fieldName] = graphql_1.defaultFieldResolver;
}
});
}
}
});
return container;
}
//
// Resolver helpers
//
function isResolveFn(value) {
return typeof value === 'function';
}
function isResolveOptions(value) {
return ((0, utils_1.isDefined)(value.resolve) ||
(0, utils_1.isDefined)(value.subscribe) ||
(0, utils_1.isDefined)(value.extensions));
}
function isScalarResolver(obj) {
return obj instanceof graphql_1.GraphQLScalarType;
}
function isEnumResolver(obj) {
return Object.values(obj).every(utils_1.isPrimitive);
}