cube-ms
Version:
Production-ready microservice framework with health monitoring, validation, error handling, and Docker Swarm support
494 lines (427 loc) • 12.5 kB
JavaScript
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;