Fix for schema-stitching issues at service startup
Some checks failed
Docker Image CI / build (push) Has been cancelled

This commit is contained in:
2026-03-26 21:02:31 -04:00
parent b753481754
commit 2e31f6cf49
3 changed files with 100 additions and 231 deletions

View File

@@ -151,7 +151,7 @@ export const typeDefs = gql`
} }
# Weekly pull list item (from League of Comic Geeks) # Weekly pull list item (from League of Comic Geeks)
type PullListItem { type MetadataPullListItem {
name: String name: String
publisher: String publisher: String
url: String url: String
@@ -165,13 +165,13 @@ export const typeDefs = gql`
} }
# Paginated pull list response # Paginated pull list response
type PullListResponse { type MetadataPullListResponse {
result: [PullListItem!]! result: [MetadataPullListItem!]!
meta: PaginationMeta! meta: MetadataPaginationMeta!
} }
# Pagination metadata # Pagination metadata
type PaginationMeta { type MetadataPaginationMeta {
currentPage: Int! currentPage: Int!
totalPages: Int! totalPages: Int!
pageSize: Int! pageSize: Int!
@@ -336,7 +336,7 @@ export const typeDefs = gql`
""" """
Get weekly pull list from League of Comic Geeks Get weekly pull list from League of Comic Geeks
""" """
getWeeklyPullList(input: WeeklyPullListInput!): PullListResponse! getWeeklyPullList(input: WeeklyPullListInput!): MetadataPullListResponse!
""" """
Fetch resource from Metron API Fetch resource from Metron API

View File

@@ -135,10 +135,10 @@ export default class ApiService extends Service {
logging: true, logging: true,
}, },
// Standalone metadata GraphQL endpoint (no stitching) // Standalone metadata GraphQL endpoint (no stitching)
// This endpoint exposes only the local metadata schema for the core-service to stitch // This endpoint exposes only the local metadata schema for external services to stitch
{ {
path: "/metadata-graphql", path: "/metadata-graphql",
whitelist: ["graphql.query"], whitelist: ["gateway.queryLocal"],
cors: { cors: {
origin: "*", origin: "*",
methods: ["GET", "POST", "OPTIONS"], methods: ["GET", "POST", "OPTIONS"],
@@ -152,7 +152,7 @@ export default class ApiService extends Service {
try { try {
const { query, variables, operationName } = req.body; const { query, variables, operationName } = req.body;
const result = await req.$ctx.broker.call("graphql.query", { const result = await req.$ctx.broker.call("gateway.queryLocal", {
query, query,
variables, variables,
operationName, operationName,
@@ -182,7 +182,7 @@ export default class ApiService extends Service {
const operationName = req.$params.operationName; const operationName = req.$params.operationName;
try { try {
const result = await req.$ctx.broker.call("graphql.query", { const result = await req.$ctx.broker.call("gateway.queryLocal", {
query, query,
variables, variables,
operationName, operationName,

View File

@@ -1,7 +1,6 @@
import { Service, ServiceBroker } from "moleculer"; import { Service, ServiceBroker } from "moleculer";
import { ApolloServer } from "@apollo/server"; import { ApolloServer } from "@apollo/server";
import { stitchSchemas } from "@graphql-tools/stitch"; import { stitchSchemas } from "@graphql-tools/stitch";
import { wrapSchema } from "@graphql-tools/wrap";
import { print, getIntrospectionQuery, buildClientSchema } from "graphql"; import { print, getIntrospectionQuery, buildClientSchema } from "graphql";
import { AsyncExecutor } from "@graphql-tools/utils"; import { AsyncExecutor } from "@graphql-tools/utils";
import axios from "axios"; import axios from "axios";
@@ -10,11 +9,11 @@ import { resolvers } from "../models/graphql/resolvers";
/** /**
* GraphQL Gateway Service with Schema Stitching * GraphQL Gateway Service with Schema Stitching
* Combines the local metadata schema with the remote GraphQL server on port 3000 * Combines the local metadata schema with the remote GraphQL server
*/ */
export default class GatewayService extends Service { export default class GatewayService extends Service {
private apolloServer?: ApolloServer; private apolloServer?: ApolloServer;
private remoteGraphQLUrl = process.env.REMOTE_GRAPHQL_URL || "http://localhost:3000/graphql"; private localApolloServer?: ApolloServer;
public constructor(broker: ServiceBroker) { public constructor(broker: ServiceBroker) {
super(broker); super(broker);
@@ -23,8 +22,6 @@ export default class GatewayService extends Service {
name: "gateway", name: "gateway",
settings: { settings: {
// Gateway endpoint path
path: "/graphql",
remoteGraphQLUrl: process.env.REMOTE_GRAPHQL_URL || "http://localhost:3000/graphql", remoteGraphQLUrl: process.env.REMOTE_GRAPHQL_URL || "http://localhost:3000/graphql",
}, },
@@ -39,53 +36,43 @@ export default class GatewayService extends Service {
operationName: { type: "string", optional: true }, operationName: { type: "string", optional: true },
}, },
async handler(ctx: any) { async handler(ctx: any) {
try { if (!this.apolloServer) {
if (!this.apolloServer) { throw new Error("Apollo Gateway Server not initialized");
throw new Error("Apollo Gateway Server not initialized");
}
const { query, variables, operationName } = ctx.params;
this.logger.debug("Executing GraphQL query through gateway:", {
operationName,
variables,
});
const response = await this.apolloServer.executeOperation(
{
query,
variables,
operationName,
},
{
contextValue: {
broker: this.broker,
ctx,
},
}
);
if (response.body.kind === "single") {
return response.body.singleResult;
}
return response;
} catch (error) {
this.logger.error("GraphQL gateway query error:", error);
throw error;
} }
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;
}, },
}, },
/** /**
* Get stitched schema information * Execute a GraphQL query against local metadata schema only
*/ */
getSchema: { queryLocal: {
async handler() { params: {
return { query: "string",
message: "Stitched schema combining local metadata service and remote GraphQL server", variables: { type: "object", optional: true },
remoteUrl: this.settings.remoteGraphQLUrl, 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;
}, },
}, },
}, },
@@ -96,43 +83,21 @@ export default class GatewayService extends Service {
*/ */
createRemoteExecutor(): AsyncExecutor { createRemoteExecutor(): AsyncExecutor {
const remoteUrl = this.settings.remoteGraphQLUrl; const remoteUrl = this.settings.remoteGraphQLUrl;
const logger = this.logger;
return async ({ document, variables, context }) => {
const query = print(document);
logger.debug(`Executing remote query to ${remoteUrl}`);
return async ({ document, variables }) => {
try { try {
const response = await axios.post( const response = await axios.post(
remoteUrl, remoteUrl,
{ { query: print(document), variables },
query, { headers: { "Content-Type": "application/json" }, timeout: 30000 }
variables,
},
{
headers: {
"Content-Type": "application/json",
},
timeout: 30000, // 30 second timeout
}
); );
return response.data; return response.data;
} catch (error: any) { } catch (error: any) {
logger.error("Remote GraphQL execution error:", error.message);
// Return a GraphQL-formatted error
return { return {
errors: [ errors: [{
{ message: `Remote server error: ${error.message}`,
message: `Failed to execute query on remote server: ${error.message}`, extensions: { code: "REMOTE_GRAPHQL_ERROR" },
extensions: { }],
code: "REMOTE_GRAPHQL_ERROR",
remoteUrl,
},
},
],
}; };
} }
}; };
@@ -142,169 +107,73 @@ export default class GatewayService extends Service {
* Initialize Apollo Server with stitched schema * Initialize Apollo Server with stitched schema
*/ */
async initApolloGateway() { async initApolloGateway() {
this.logger.info("Initializing Apollo Gateway with Schema Stitching..."); 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 { try {
// Create executor for remote schema const response = await axios.post(
const remoteExecutor = this.createRemoteExecutor(); this.settings.remoteGraphQLUrl,
{ query: getIntrospectionQuery() },
// Try to introspect the remote schema { headers: { "Content-Type": "application/json" }, timeout: 30000 }
let remoteSchema; );
try {
this.logger.info(`Attempting to introspect remote schema at ${this.remoteGraphQLUrl}`);
// Manually introspect the remote schema
const introspectionQuery = getIntrospectionQuery();
const introspectionResult = await remoteExecutor({
document: { kind: 'Document', definitions: [] } as any,
variables: {},
context: {},
});
// Fetch introspection via direct query
const response = await axios.post(
this.remoteGraphQLUrl,
{ query: introspectionQuery },
{
headers: { "Content-Type": "application/json" },
timeout: 30000,
}
);
if (response.data.errors) {
throw new Error(`Introspection failed: ${JSON.stringify(response.data.errors)}`);
}
if (!response.data.errors) {
remoteSchema = buildClientSchema(response.data.data); remoteSchema = buildClientSchema(response.data.data);
this.logger.info("Successfully introspected remote schema"); this.logger.info("Remote schema introspected successfully");
} catch (error: any) {
this.logger.warn(
`Could not introspect remote schema at ${this.remoteGraphQLUrl}: ${error.message}`
);
this.logger.warn("Gateway will start with local schema only. Remote schema will be unavailable.");
remoteSchema = null;
} }
// Create local executable schema
const { makeExecutableSchema } = await import("@graphql-tools/schema");
const localSchema = makeExecutableSchema({
typeDefs,
resolvers: {
Query: {
...resolvers.Query,
},
Mutation: {
...resolvers.Mutation,
},
JSON: resolvers.JSON,
},
});
// Stitch schemas together
let stitchedSchema;
if (remoteSchema) {
this.logger.info("Stitching local and remote schemas together...");
stitchedSchema = stitchSchemas({
subschemas: [
{
schema: localSchema,
executor: async ({ document, variables, context }) => {
// Execute local queries through Moleculer broker
const query = print(document);
const broker = context?.broker || this.broker;
// Parse the query to determine which resolver to call
// For now, we'll execute through the local resolvers directly
const result = await this.executeLocalQuery(query, variables, context);
return result;
},
},
{
schema: remoteSchema,
executor: remoteExecutor,
},
],
mergeTypes: true, // Merge types with the same name
});
this.logger.info("Schema stitching completed successfully");
} else {
this.logger.info("Using local schema only (remote unavailable)");
stitchedSchema = localSchema;
}
// Create Apollo Server with stitched schema
this.apolloServer = new ApolloServer({
schema: stitchedSchema,
introspection: true,
formatError: (error) => {
this.logger.error("GraphQL Gateway Error:", error);
return {
message: error.message,
locations: error.locations,
path: error.path,
extensions: {
code: error.extensions?.code,
stacktrace:
process.env.NODE_ENV === "development"
? error.extensions?.stacktrace
: undefined,
},
};
},
});
await this.apolloServer.start();
this.logger.info("Apollo Gateway Server started successfully");
} catch (error: any) { } catch (error: any) {
this.logger.error("Failed to initialize Apollo Gateway:", error); this.logger.warn(`Remote schema unavailable: ${error.message}`);
throw error;
}
},
/**
* Execute local queries through Moleculer actions
*/
async executeLocalQuery(query: string, variables: any, context: any) {
// This is a simplified implementation
// In production, you'd want more sophisticated query parsing
const broker = context?.broker || this.broker;
// Determine which action to call based on the query
// This is a basic implementation - you may need to enhance this
if (query.includes("searchComicVine")) {
const result = await broker.call("comicvine.search", variables.input);
return { data: { searchComicVine: result } };
} else if (query.includes("volumeBasedSearch")) {
const result = await broker.call("comicvine.volumeBasedSearch", variables.input);
return { data: { volumeBasedSearch: result } };
} else if (query.includes("getIssuesForSeries")) {
const result = await broker.call("comicvine.getIssuesForSeries", {
comicObjectId: variables.comicObjectId,
});
return { data: { getIssuesForSeries: result } };
} else if (query.includes("getWeeklyPullList")) {
const result = await broker.call("comicvine.getWeeklyPullList", variables.input);
return { data: { getWeeklyPullList: result } };
} else if (query.includes("getVolume")) {
const result = await broker.call("comicvine.getVolume", variables.input);
return { data: { getVolume: result } };
} else if (query.includes("fetchMetronResource")) {
const result = await broker.call("metron.fetchResource", variables.input);
return { data: { fetchMetronResource: result } };
} }
return { data: null }; // 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 * Stop Apollo Gateway Server
*/ */
async stopApolloGateway() { async stopApolloGateway() {
if (this.localApolloServer) {
await this.localApolloServer.stop();
this.localApolloServer = undefined;
}
if (this.apolloServer) { if (this.apolloServer) {
this.logger.info("Stopping Apollo Gateway Server...");
await this.apolloServer.stop(); await this.apolloServer.stop();
this.apolloServer = undefined; this.apolloServer = undefined;
this.logger.info("Apollo Gateway Server stopped");
} }
}, },
}, },