194 lines
5.6 KiB
TypeScript
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();
|
|
},
|
|
});
|
|
}
|
|
}
|