UNPKG

cube-ms

Version:

Production-ready microservice framework with health monitoring, validation, error handling, and Docker Swarm support

494 lines (427 loc) 12.5 kB
import { ApolloServer } from '@apollo/server'; import { expressMiddleware } from '@apollo/server/express4'; import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { GraphQLScalarType, GraphQLError } from 'graphql'; import { DateTimeResolver, DateTimeTypeDefinition } from 'graphql-scalars'; import depthLimit from 'graphql-depth-limit'; import costAnalysis from 'graphql-cost-analysis'; import { logRequest, logError } from '../ccn-logger.js'; /** * GraphQL Server Implementation for Cube Microservices * Provides flexible, efficient API with built-in security and monitoring */ export class GraphQLServer { constructor(options = {}) { this.config = { maxDepth: options.maxDepth || 10, maxCost: options.maxCost || 1000, enableIntrospection: options.enableIntrospection !== false, enablePlayground: options.enablePlayground !== false, enableMetrics: options.enableMetrics !== false, enableCaching: options.enableCaching !== false, cacheTtl: options.cacheTtl || 300, // 5 minutes rateLimiting: options.rateLimiting || { max: 100, window: 60000 // 1 minute }, ...options }; this.typeDefs = []; this.resolvers = []; this.plugins = []; this.context = {}; this.server = null; } /** * Add GraphQL type definitions */ addTypeDefs(typeDefs) { if (Array.isArray(typeDefs)) { this.typeDefs.push(...typeDefs); } else { this.typeDefs.push(typeDefs); } return this; } /** * Add GraphQL resolvers */ addResolvers(resolvers) { if (Array.isArray(resolvers)) { this.resolvers.push(...resolvers); } else { this.resolvers.push(resolvers); } return this; } /** * Add custom plugins */ addPlugin(plugin) { this.plugins.push(plugin); return this; } /** * Set context function or object */ setContext(contextFn) { this.context = contextFn; return this; } /** * Initialize Apollo Server */ async initialize(httpServer) { // Merge all type definitions with base schema const baseTypeDefs = ` scalar DateTime scalar JSON type Query { _health: String _schema: String } type Mutation { _noop: String } type Subscription { _ping: String } `; const allTypeDefs = [baseTypeDefs, ...this.typeDefs]; // Merge all resolvers with base resolvers const baseResolvers = { DateTime: DateTimeResolver, JSON: new GraphQLScalarType({ name: 'JSON', description: 'JSON custom scalar type', serialize: (value) => value, parseValue: (value) => value, parseLiteral: (ast) => JSON.parse(ast.value) }), Query: { _health: () => 'OK', _schema: (_, __, { schema }) => schema.toString() }, Mutation: { _noop: () => 'OK' }, Subscription: { _ping: { subscribe: () => { const asyncIterator = { [Symbol.asyncIterator]: async function* () { while (true) { yield { _ping: new Date().toISOString() }; await new Promise(resolve => setTimeout(resolve, 5000)); } } }; return asyncIterator; } } } }; const mergedResolvers = this.mergeResolvers([baseResolvers, ...this.resolvers]); // Create executable schema const schema = makeExecutableSchema({ typeDefs: allTypeDefs, resolvers: mergedResolvers }); // Setup built-in plugins const plugins = [ ...this.getBuiltInPlugins(httpServer), ...this.plugins ]; // Create Apollo Server this.server = new ApolloServer({ schema, plugins, introspection: this.config.enableIntrospection, formatError: this.formatError.bind(this), validationRules: this.getValidationRules() }); await this.server.start(); return this.server; } /** * Get Express middleware */ getMiddleware() { if (!this.server) { throw new Error('GraphQL server not initialized. Call initialize() first.'); } return expressMiddleware(this.server, { context: this.buildContext.bind(this) }); } /** * Build context for each request */ async buildContext({ req, res }) { const startTime = Date.now(); // Log GraphQL request if (this.config.enableMetrics) { logRequest('graphql_request', 'GraphQL request received', { operation: req.body?.operationName, query: req.body?.query?.substring(0, 200), variables: Object.keys(req.body?.variables || {}), userAgent: req.headers['user-agent'] }); } const baseContext = { req, res, startTime, user: req.user, // From authentication middleware logger: { info: (message, meta) => logRequest('graphql_info', message, meta), error: (message, error, meta) => logError('graphql_error', message, error, meta) } }; // Apply custom context if (typeof this.context === 'function') { const customContext = await this.context({ req, res }); return { ...baseContext, ...customContext }; } return { ...baseContext, ...this.context }; } /** * Get built-in plugins */ getBuiltInPlugins(httpServer) { const plugins = []; // HTTP server drain plugin if (httpServer) { plugins.push(ApolloServerPluginDrainHttpServer({ httpServer })); } // Metrics plugin if (this.config.enableMetrics) { plugins.push(this.createMetricsPlugin()); } // Performance monitoring plugin plugins.push(this.createPerformancePlugin()); // Error handling plugin plugins.push(this.createErrorPlugin()); return plugins; } /** * Create metrics collection plugin */ createMetricsPlugin() { return { requestDidStart: () => ({ didResolveOperation: (requestContext) => { const { operationName, operation } = requestContext.request; logRequest('graphql_operation', 'GraphQL operation resolved', { operationName, operationType: operation?.operation, fieldCount: this.countFields(operation) }); }, willSendResponse: (requestContext) => { const { startTime } = requestContext.contextValue; const duration = Date.now() - startTime; logRequest('graphql_response', 'GraphQL response sent', { operationName: requestContext.request.operationName, duration: `${duration}ms`, errors: requestContext.errors?.length || 0, extensions: Object.keys(requestContext.response.extensions || {}) }); } }) }; } /** * Create performance monitoring plugin */ createPerformancePlugin() { return { requestDidStart: () => ({ didResolveField: ({ info }) => { const start = Date.now(); return (error, result) => { const duration = Date.now() - start; // Log slow resolvers if (duration > 1000) { // > 1 second logRequest('graphql_slow_resolver', 'Slow GraphQL resolver detected', { fieldName: info.fieldName, parentType: info.parentType.name, duration: `${duration}ms`, path: info.path }); } }; } }) }; } /** * Create error handling plugin */ createErrorPlugin() { return { requestDidStart: () => ({ didEncounterErrors: (requestContext) => { for (const error of requestContext.errors) { if (!(error.originalError instanceof GraphQLError)) { logError('graphql_resolver_error', 'GraphQL resolver error', error, { operationName: requestContext.request.operationName, path: error.path, locations: error.locations }); } } } }) }; } /** * Get validation rules for security */ getValidationRules() { const rules = []; // Depth limiting rules.push(depthLimit(this.config.maxDepth)); // Cost analysis (if enabled) if (this.config.maxCost) { rules.push(costAnalysis({ maximumCost: this.config.maxCost, onComplete: (cost) => { logRequest('graphql_cost', 'GraphQL query cost calculated', { cost, maxCost: this.config.maxCost }); } })); } return rules; } /** * Format errors for client response */ formatError(formattedError, error) { // Log internal errors if (error.originalError && !error.originalError.expose) { logError('graphql_internal_error', 'Internal GraphQL error', error.originalError, { path: error.path, locations: error.locations }); // Hide internal errors in production if (process.env.NODE_ENV === 'production') { return new GraphQLError('Internal server error', { code: 'INTERNAL_ERROR' }); } } return formattedError; } /** * Merge multiple resolver objects */ mergeResolvers(resolvers) { const merged = {}; for (const resolver of resolvers) { for (const [typeName, typeResolvers] of Object.entries(resolver)) { if (!merged[typeName]) { merged[typeName] = {}; } if (typeof typeResolvers === 'object' && typeResolvers !== null) { Object.assign(merged[typeName], typeResolvers); } else { merged[typeName] = typeResolvers; } } } return merged; } /** * Count fields in GraphQL operation */ countFields(operation) { if (!operation || !operation.selectionSet) return 0; let count = 0; const countSelections = (selectionSet) => { for (const selection of selectionSet.selections) { count++; if (selection.selectionSet) { countSelections(selection.selectionSet); } } }; countSelections(operation.selectionSet); return count; } /** * Stop the GraphQL server */ async stop() { if (this.server) { await this.server.stop(); } } } /** * GraphQL Schema Builder Helper */ export class GraphQLSchemaBuilder { constructor() { this.queries = []; this.mutations = []; this.subscriptions = []; this.types = []; this.resolvers = {}; } addQuery(name, type, resolver, description) { this.queries.push({ name, type, description }); this.resolvers.Query = this.resolvers.Query || {}; this.resolvers.Query[name] = resolver; return this; } addMutation(name, type, resolver, description) { this.mutations.push({ name, type, description }); this.resolvers.Mutation = this.resolvers.Mutation || {}; this.resolvers.Mutation[name] = resolver; return this; } addSubscription(name, type, resolver, description) { this.subscriptions.push({ name, type, description }); this.resolvers.Subscription = this.resolvers.Subscription || {}; this.resolvers.Subscription[name] = resolver; return this; } addType(typeDef) { this.types.push(typeDef); return this; } addTypeResolver(typeName, resolver) { this.resolvers[typeName] = resolver; return this; } build() { const queries = this.queries.map(q => `${q.description ? `"""${q.description}"""` : ''}\n${q.name}: ${q.type}` ).join('\n '); const mutations = this.mutations.map(m => `${m.description ? `"""${m.description}"""` : ''}\n${m.name}: ${m.type}` ).join('\n '); const subscriptions = this.subscriptions.map(s => `${s.description ? `"""${s.description}"""` : ''}\n${s.name}: ${s.type}` ).join('\n '); const typeDefs = ` ${this.types.join('\n')} type Query { ${queries} } type Mutation { ${mutations} } type Subscription { ${subscriptions} } `; return { typeDefs, resolvers: this.resolvers }; } } export { GraphQLError } from 'graphql'; export default GraphQLServer;