Files
threetwo-metadata-service/services/gateway.service.ts
Rishi Ghan 2e31f6cf49
Some checks failed
Docker Image CI / build (push) Has been cancelled
Fix for schema-stitching issues at service startup
2026-03-26 21:02:31 -04:00

194 lines
5.6 KiB
TypeScript

import { Service, ServiceBroker } from "moleculer";
import { ApolloServer } from "@apollo/server";
import { stitchSchemas } from "@graphql-tools/stitch";
import { print, getIntrospectionQuery, buildClientSchema } from "graphql";
import { AsyncExecutor } from "@graphql-tools/utils";
import axios from "axios";
import { typeDefs } from "../models/graphql/typedef";
import { resolvers } from "../models/graphql/resolvers";
/**
* GraphQL Gateway Service with Schema Stitching
* Combines the local metadata schema with the remote GraphQL server
*/
export default class GatewayService extends Service {
private apolloServer?: ApolloServer;
private localApolloServer?: ApolloServer;
public constructor(broker: ServiceBroker) {
super(broker);
this.parseServiceSchema({
name: "gateway",
settings: {
remoteGraphQLUrl: process.env.REMOTE_GRAPHQL_URL || "http://localhost:3000/graphql",
},
actions: {
/**
* Execute a GraphQL query through the stitched schema
*/
query: {
params: {
query: "string",
variables: { type: "object", optional: true },
operationName: { type: "string", optional: true },
},
async handler(ctx: any) {
if (!this.apolloServer) {
throw new Error("Apollo Gateway Server not initialized");
}
const { query, variables, operationName } = ctx.params;
const response = await this.apolloServer.executeOperation(
{ query, variables, operationName },
{ contextValue: { broker: this.broker, ctx } }
);
return response.body.kind === "single" ? response.body.singleResult : response;
},
},
/**
* Execute a GraphQL query against local metadata schema only
*/
queryLocal: {
params: {
query: "string",
variables: { type: "object", optional: true },
operationName: { type: "string", optional: true },
},
async handler(ctx: any) {
if (!this.localApolloServer) {
throw new Error("Local Apollo Server not initialized");
}
const { query, variables, operationName } = ctx.params;
const response = await this.localApolloServer.executeOperation(
{ query, variables, operationName },
{ contextValue: { broker: this.broker, ctx } }
);
return response.body.kind === "single" ? response.body.singleResult : response;
},
},
},
methods: {
/**
* Create an executor for the remote GraphQL server
*/
createRemoteExecutor(): AsyncExecutor {
const remoteUrl = this.settings.remoteGraphQLUrl;
return async ({ document, variables }) => {
try {
const response = await axios.post(
remoteUrl,
{ query: print(document), variables },
{ headers: { "Content-Type": "application/json" }, timeout: 30000 }
);
return response.data;
} catch (error: any) {
return {
errors: [{
message: `Remote server error: ${error.message}`,
extensions: { code: "REMOTE_GRAPHQL_ERROR" },
}],
};
}
};
},
/**
* Initialize Apollo Server with stitched schema
*/
async initApolloGateway() {
this.logger.info("Initializing Apollo Gateway...");
const { makeExecutableSchema } = await import("@graphql-tools/schema");
const { execute } = await import("graphql");
// Create local schema
const localSchema = makeExecutableSchema({ typeDefs, resolvers });
// Create standalone local Apollo Server for /metadata-graphql endpoint
this.localApolloServer = new ApolloServer({ schema: localSchema, introspection: true });
await this.localApolloServer.start();
this.logger.info("Local metadata Apollo Server started");
// Create local executor
const localExecutor: AsyncExecutor = async ({ document, variables, context }) => {
return execute({
schema: localSchema,
document,
variableValues: variables,
contextValue: { broker: context?.broker || this.broker, ctx: context?.ctx },
}) as any;
};
// Try to introspect remote schema
let remoteSchema = null;
try {
const response = await axios.post(
this.settings.remoteGraphQLUrl,
{ query: getIntrospectionQuery() },
{ headers: { "Content-Type": "application/json" }, timeout: 30000 }
);
if (!response.data.errors) {
remoteSchema = buildClientSchema(response.data.data);
this.logger.info("Remote schema introspected successfully");
}
} catch (error: any) {
this.logger.warn(`Remote schema unavailable: ${error.message}`);
}
// Stitch schemas or use local only
const schema = remoteSchema
? stitchSchemas({
subschemas: [
{ schema: localSchema, executor: localExecutor },
{ schema: remoteSchema, executor: this.createRemoteExecutor() },
],
mergeTypes: false,
})
: localSchema;
this.apolloServer = new ApolloServer({ schema, introspection: true });
await this.apolloServer.start();
this.logger.info("Apollo Gateway started");
},
/**
* Stop Apollo Gateway Server
*/
async stopApolloGateway() {
if (this.localApolloServer) {
await this.localApolloServer.stop();
this.localApolloServer = undefined;
}
if (this.apolloServer) {
await this.apolloServer.stop();
this.apolloServer = undefined;
}
},
},
/**
* Service lifecycle hooks
*/
started: async function (this: any) {
await this.initApolloGateway();
},
stopped: async function (this: any) {
await this.stopApolloGateway();
},
});
}
}