UNPKG

moleculer-api

Version:

A dynamic API Gateway for MoleculerJS which updates REST endpoints and aggregated GraphQL schema, access control policy for each action calls from metadata of remote services schema without restart or deployment.

526 lines 29.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.GraphQLProtocolPlugin = void 0; const tslib_1 = require("tslib"); const _ = tslib_1.__importStar(require("lodash")); const interface_1 = require("../../../../interface"); const server_1 = require("../../../../server"); const connector_1 = require("../../connector"); const plugin_1 = require("../plugin"); const language_1 = require("graphql/language"); const handler_1 = require("./handler"); class GraphQLProtocolPlugin extends plugin_1.ProtocolPlugin { constructor(props, opts) { super(props); this.props = props; this.opts = _.defaultsDeep(opts || {}, GraphQLProtocolPlugin.autoLoadOptions); } start() { return tslib_1.__awaiter(this, void 0, void 0, function* () { }); } stop() { return tslib_1.__awaiter(this, void 0, void 0, function* () { }); } validateSchema(schema) { const typeDefs = []; let typeDefsString = ""; const result = interface_1.validateObject(schema, { description: { type: "string", optional: true, }, typeDefs: { type: "custom", check(value) { // parse graphql.documentNode which might have been generated with gql` ... ` if (typeof value === "object" && value !== null) { try { typeDefsString = value = language_1.print(value); } catch (error) { return [{ field: `typeDefs`, type: "typeDefsSyntax", message: error.message, actual: value, }]; } } // parse string if (typeof value !== "string") { return [{ field: `typeDefs`, type: "typeDefsSyntax", message: "type definitions must be string or valid graphql.DocumentNode", actual: value, }]; } const errors = []; try { // parse GraphQL Schema const doc = language_1.parse(value, { noLocation: false }); for (const def of doc.definitions) { if (!GraphQLProtocolPlugin.allowedDefKinds.includes(def.kind)) { errors.push({ field: `typeDefs`, type: "typeDefsForbidden", // @ts-ignore message: `${def.name ? `${def.name.value} (${def.kind})` : def.kind} is not allowed`, actual: language_1.print(def), }); } else { typeDefs.push(def); } } } catch (error) { // parsing error const location = error.locations && error.locations[0] || null; errors.push({ field: `typeDefs`, type: "typeDefsSyntax", message: error.message.replace("Syntax Error: ", ""), actual: value, location, }); } return errors.length === 0 ? true : errors; }, }, resolvers: { type: "custom", check(value) { if (typeof value !== "object" || value === null) { return [{ field: `resolvers`, type: "resolversInvalid", message: "type resolvers should be an object", actual: value, }]; } const errors = []; const typeDefsByName = typeDefs.reduce((obj, typeDef) => { const typeName = typeDef.name.value; if (!obj[typeName]) { obj[typeName] = []; } obj[typeName].push(typeDef); return obj; }, {}); for (const [typeName, partialTypeDefs] of Object.entries(typeDefsByName)) { const typeResolver = value[typeName]; // not a type which can have resolver if (!partialTypeDefs.every(def => GraphQLProtocolPlugin.resolverAllowedDefKinds.includes(def.kind))) { // this type should not have resolver if (typeof typeResolver !== "undefined") { errors.push({ field: `resolvers.${typeName}`, type: "typeResolverInvalid", message: `${typeName} cannot have a type resolver`, actual: typeResolver, expected: undefined, }); } continue; } const typeFieldNames = partialTypeDefs.reduce((fieldNames, typeDef) => { if (typeDef.fields) { fieldNames.push(...typeDef.fields.map(f => f.name.value)); } return fieldNames; }, []); const typeForbiddenFieldNames = typeFieldNames.filter(f => GraphQLProtocolPlugin.forbiddenFieldNames.includes(f)); // type has forbidden fields if (typeForbiddenFieldNames.length > 0) { errors.push({ field: `typeDefs`, type: "typeDefsForbidden", message: `${typeName} cannot define preserved fields`, actual: typeForbiddenFieldNames, expected: undefined, }); continue; } // __isTypeOf field resolver is required for interface implementation const interfaceNames = partialTypeDefs.reduce((names, typeDef) => { if (typeDef.interfaces) { names.push(...typeDef.interfaces.map(i => i.name.value)); } return names; }, []); const isTypeOfFieldResolverRequired = interfaceNames.length > 0; if (isTypeOfFieldResolverRequired) { typeFieldNames.push(GraphQLProtocolPlugin.isTypeOfFieldName); } // type has no resolver if (typeof typeResolver !== "object" || typeResolver === null) { // this type should have resolver if (GraphQLProtocolPlugin.resolverRequiredTypeNames.includes(typeName) || isTypeOfFieldResolverRequired) { errors.push({ field: `resolvers.${typeName}`, type: "typeResolverRequired", message: `${typeName} should have a type resolver`, actual: typeResolver, expected: `${typeFieldNames.length > 0 ? `{ [fieldName in "${typeFieldNames.join(`" | "`)}"]: GraphQLFieldResolverSchema }` : "{}"} }`, }); } continue; } // all fields of resolvers should have been defined on typeDef const typeResolverKeys = Object.keys(typeResolver); for (const resolverFieldName of typeResolverKeys) { if (!typeFieldNames.includes(resolverFieldName)) { errors.push({ field: `resolvers.${typeName}.${resolverFieldName}`, type: "fieldResolverUnnecessary", message: `${typeName} have an unnecessary field resolver for ${resolverFieldName}`, actual: typeResolver[resolverFieldName], expected: undefined, }); } } // check field resolvers for (const fieldName of typeFieldNames) { const fieldResolver = typeResolver[fieldName]; // check __isTypeOf field resolver if (fieldName === GraphQLProtocolPlugin.isTypeOfFieldName) { if (typeof fieldResolver !== "string") { errors.push({ field: `resolvers.${typeName}.${fieldName}`, type: typeof fieldResolver === "undefined" ? "fieldResolverRequired" : "fieldResolverInvalid", message: `${typeName} should have an ${fieldName} field resolver which is a string denotes a JavaScript function to distinguish the type among types which implement interfaces: ${interfaceNames.join(", ")}`, actual: fieldResolver, expected: `GraphQLIsTypeOfFieldResolverSchema`, }); } continue; } // check regular field resolver let rule; switch (typeName) { case "Mutation": if (typeof fieldResolver === "object" && fieldResolver !== null && typeof fieldResolver.call !== "undefined") { rule = { type: "object", strict: true, props: { call: connector_1.ConnectorValidator.call, }, messages: { objectStrict: "GraphQLCallableFieldResolverSchema cannot be with other connectors", }, }; } else if (typeof fieldResolver === "object" && fieldResolver !== null && typeof fieldResolver.publish !== "undefined") { rule = { type: "object", strict: true, props: { publish: connector_1.ConnectorValidator.publish, }, messages: { objectStrict: "GraphQLPublishableFieldResolverSchema cannot be with other connectors", }, }; } else if (typeof fieldResolver === "string") { rule = connector_1.ConnectorValidator.map; } else { errors.push({ field: `resolvers.${typeName}.${fieldName}`, type: typeof fieldResolver === "undefined" ? "fieldResolverRequired" : "fieldResolverInvalid", message: `${typeName} should have an ${fieldName} field resolver which is an object having either call, publish property or a string which denotes a JavaScript function`, expected: `Omit<GraphQLCallableFieldResolverSchema, "ignoreError"> | GraphQLPublishableFieldResolverSchema | GraphQLMappableFieldResolverSchema`, }); } break; case "Subscription": if (typeof fieldResolver === "object" && fieldResolver !== null && typeof fieldResolver.subscribe !== "undefined") { rule = { type: "object", strict: true, props: { subscribe: connector_1.ConnectorValidator.subscribe, }, messages: { objectStrict: "GraphQLCallableFieldResolverSchema cannot be with other connectors", }, }; } else { errors.push({ field: `resolvers.${typeName}.${fieldName}`, type: typeof fieldResolver === "undefined" ? "fieldResolverRequired" : "fieldResolverInvalid", message: `${typeName} should have an ${fieldName} field resolver which is an object having subscribe property`, expected: `GraphQLSubscribableFieldResolverSchema`, }); } break; case "Query": default: if (typeof fieldResolver === "object" && fieldResolver !== null && typeof fieldResolver.call !== "undefined") { rule = { type: "object", strict: true, props: { call: connector_1.ConnectorValidator.call, ignoreError: { type: "boolean", optional: true, }, }, messages: { objectStrict: "GraphQLCallableFieldResolverSchema cannot be with other connectors", }, }; } else if (typeof fieldResolver === "string") { rule = connector_1.ConnectorValidator.map; } else { // regular types can omit field resolvers (use default field resolver) if (typeof fieldResolver === "undefined" && typeName !== "Query") { break; } errors.push({ field: `resolvers.${typeName}.${fieldName}`, type: typeof fieldResolver === "undefined" ? "fieldResolverRequired" : "fieldResolverInvalid", message: `${typeName} should have an ${fieldName} field resolver which is either an object having call property or a string which denotes a JavaScript function`, expected: `GraphQLCallableFieldResolverSchema | GraphQLMappableFieldResolverSchema`, }); } break; } // @ts-ignore if (rule) { errors.push(...interface_1.validateValue(fieldResolver, // @ts-ignore rule, { strict: true, field: `resolvers.${typeName}.${fieldName}`, })); } } // end-of: typeFieldNames for-loop } // end-of: typeDefs for-loop return errors.length === 0 ? true : errors; }, }, }, { strict: true, }); // convert original typeDefs as string too for human-readable information if (typeDefsString) { schema.typeDefs = typeDefsString; } return result; } compileSchemata(routeHashMapCache, integrations, branch) { const items = new Array(); /* calculate integrated hash to fetch cached handlers */ const hashes = []; for (const integration of integrations) { const schema = integration.schema.protocol[this.key]; // the source object below hash contains properties which can make this route unique hashes.push(interface_1.hashObject([schema.typeDefs, schema.resolvers, integration.service.hash], true)); } const routeHash = interface_1.hashObject(hashes, false); const subscriptionRouteHash = `${routeHash}@subscription`; const playgroundRouteHash = `static@graphql-playground`; // cache hit const cachedRoute = routeHashMapCache.get(routeHash); const cachedSubscriptionRoute = routeHashMapCache.get(subscriptionRouteHash); const cachedPlaygroundRoute = routeHashMapCache.get(playgroundRouteHash); if (cachedRoute) { items.push({ hash: routeHash, route: cachedRoute }); // both playground and subscription handlers are optional if (cachedSubscriptionRoute) { items.push({ hash: subscriptionRouteHash, route: cachedSubscriptionRoute }); } if (cachedPlaygroundRoute) { items.push({ hash: playgroundRouteHash, route: cachedPlaygroundRoute }); } return items; } // create new GraphQL routes const { route, subscriptionRoute, playgroundRoute } = this.createGraphQLHandlers(integrations); items.push({ hash: routeHash, route }); // both playground and subscription handlers are optional if (subscriptionRoute) { items.push({ hash: subscriptionRouteHash, route: subscriptionRoute }); } if (playgroundRoute) { items.push({ hash: playgroundRouteHash, route: playgroundRoute }); } return items; } // TODO: GraphQL Plugin catalog describeSchema(schema) { return {}; } // @throwable createGraphQLHandlers(integrations) { const typeDefs = []; let resolvers = {}; for (const integration of integrations) { const schema = integration.schema.protocol[this.key]; typeDefs.push(typeof schema.typeDefs === "string" ? schema.typeDefs : language_1.print(schema.typeDefs)); resolvers = _.merge(resolvers, this.createGraphQLResolvers(schema.resolvers, integration)); } // merge base typeDefs if (this.opts.typeDefs) { typeDefs.push(...(Array.isArray(this.opts.typeDefs) ? this.opts.typeDefs : [this.opts.typeDefs]) .map(defs => typeof defs === "string" ? defs : language_1.print(defs))); } const resolversList = [resolvers]; if (this.opts.resolvers) { resolversList.push(...(Array.isArray(this.opts.resolvers) ? this.opts.resolvers : [this.opts.resolvers])); } const { handler, subscriptionHandler, playgroundHandler } = new handler_1.GraphQLHandlers((message) => { this.props.logger.error(message); }, Object.assign(Object.assign({}, this.opts), { typeDefs, resolvers: resolversList })); return { route: new server_1.HTTPRoute({ method: "POST", path: "/graphql", description: "GraphQL HTTP operation endpoint", handler, }), subscriptionRoute: subscriptionHandler ? new server_1.WebSocketRoute({ path: "/graphql", description: "GraphQL WebSocket operation endpoint", handler: subscriptionHandler, }) : undefined, playgroundRoute: playgroundHandler ? new server_1.HTTPRoute({ method: "GET", path: "/graphql", description: "GraphQL Playground endpoint", handler: playgroundHandler, }) : undefined, }; } createGraphQLResolvers(resolversSchema, integration) { const resolvers = {}; const { Query, Mutation, Subscription } = resolversSchema, ObjectTypes = tslib_1.__rest(resolversSchema, ["Query", "Mutation", "Subscription"]); // create query resolver if (Query) { const typeResolver = resolvers.Query = {}; for (const [fieldName, fieldSchema] of Object.entries(Query)) { if (typeof fieldSchema === "string") { typeResolver[fieldName] = this.createGraphQLFieldResolverFromMapConnectorSchema(fieldSchema, integration); } else if (fieldSchema && fieldSchema.call) { typeResolver[fieldName] = this.createGraphQLFieldResolverFromCallConnectorSchema(fieldSchema, integration); } } } // create mutation resolver if (Mutation) { const typeResolver = resolvers.Mutation = {}; for (const [fieldName, fieldSchema] of Object.entries(Mutation)) { if (typeof fieldSchema === "string") { typeResolver[fieldName] = this.createGraphQLFieldResolverFromMapConnectorSchema(fieldSchema, integration); } else if (fieldSchema && fieldSchema.call) { typeResolver[fieldName] = this.createGraphQLFieldResolverFromCallConnectorSchema(fieldSchema, integration); } else if (fieldSchema && fieldSchema.publish) { typeResolver[fieldName] = this.createGraphQLFieldResolverFromPublishConnectorSchema(fieldSchema, integration); } } } // create subscription resolver if (Subscription) { const typeResolver = resolvers.Subscription = {}; for (const [fieldName, fieldSchema] of Object.entries(Subscription)) { if (fieldSchema && fieldSchema.subscribe) { typeResolver[fieldName] = this.createGraphQLFieldResolverFromSubscribeConnectorSchema(fieldSchema, integration); } } } // create other object resolvers for (const [typeName, ObjectType] of Object.entries(ObjectTypes)) { const typeResolver = resolvers[typeName] = {}; for (const [fieldName, fieldSchema] of Object.entries(ObjectType)) { if (typeof fieldSchema === "string") { if (fieldName === GraphQLProtocolPlugin.isTypeOfFieldName) { typeResolver[fieldName] = this.createGraphQLIsTypeOfFnFromMapConnectorSchema(fieldSchema, integration); } else { typeResolver[fieldName] = this.createGraphQLFieldResolverFromMapConnectorSchema(fieldSchema, integration); } } else if (fieldSchema && fieldSchema.call) { typeResolver[fieldName] = this.createGraphQLFieldResolverFromCallConnectorSchema(fieldSchema, integration); } } } return resolvers; } createGraphQLFieldResolverFromMapConnectorSchema(schema, integration) { const mapConnector = connector_1.ConnectorCompiler.map(schema, integration, { mappableKeys: ["context", "source", "args", "info"], }); return (source, args, context, info) => mapConnector({ context, source, args, info }); } createGraphQLIsTypeOfFnFromMapConnectorSchema(schema, integration) { const mapConnector = connector_1.ConnectorCompiler.map(schema, integration, { mappableKeys: ["context", "source", "info"], }); return (source, context, info) => mapConnector({ context, source, info }); } createGraphQLFieldResolverFromCallConnectorSchema(schema, integration) { const callConnector = connector_1.ConnectorCompiler.call(schema.call, integration, this.props.policyPlugins, { explicitMappableKeys: ["context", "source", "args", "info"], implicitMappableKeys: ["source", "args"], batchingEnabled: true, disableCache: false, }); const { ignoreError } = schema; return (source, args, context, info) => { const mappableArgs = { source, args, context, info }; return callConnector(context, mappableArgs) .catch(error => { if (ignoreError) { return null; } else { throw error; } }); }; } createGraphQLFieldResolverFromPublishConnectorSchema(schema, integration) { const publishConnector = connector_1.ConnectorCompiler.publish(schema.publish, integration, this.props.policyPlugins, { mappableKeys: ["context", "source", "args", "info"], }); return (source, args, context, info) => publishConnector(context, { context, source, args, info }); } createGraphQLFieldResolverFromSubscribeConnectorSchema(schema, integration) { const subscribeConnector = connector_1.ConnectorCompiler.subscribe(schema.subscribe, integration, this.props.policyPlugins, { mappableKeys: ["context", "source", "args", "info"], getAsyncIterator: true, }); return { subscribe: (source, args, context, info) => subscribeConnector(context, { context, source, args, info }, null), }; } } exports.GraphQLProtocolPlugin = GraphQLProtocolPlugin; GraphQLProtocolPlugin.key = "GraphQL"; GraphQLProtocolPlugin.autoLoadOptions = handler_1.defaultGraphQLHandlersOptions; GraphQLProtocolPlugin.allowedDefKinds = [ "ObjectTypeDefinition", "ObjectTypeExtension", "InterfaceTypeDefinition", "InterfaceTypeExtension", "UnionTypeDefinition", "UnionTypeExtension", "EnumTypeDefinition", "EnumTypeExtension", "InputObjectTypeDefinition", ]; GraphQLProtocolPlugin.resolverAllowedDefKinds = ["ObjectTypeDefinition", "ObjectTypeExtension"]; GraphQLProtocolPlugin.resolverRequiredTypeNames = ["Query", "Mutation", "Subscription"]; GraphQLProtocolPlugin.isTypeOfFieldName = "__isTypeOf"; GraphQLProtocolPlugin.forbiddenFieldNames = ["__isTypeOf", "__resolveType", "isTypeOf", "resolveType"]; //# sourceMappingURL=plugin.js.map