diff --git a/models/graphql/resolvers.ts b/models/graphql/resolvers.ts index f0973dd..195b9c3 100644 --- a/models/graphql/resolvers.ts +++ b/models/graphql/resolvers.ts @@ -622,6 +622,58 @@ export const resolvers = { } }, + /** + * Get import statistics for a directory + * @async + * @function getImportStatistics + * @param {any} _ - Parent resolver (unused) + * @param {Object} args - Query arguments + * @param {string} [args.directoryPath] - Optional directory path to analyze + * @param {Object} context - GraphQL context with broker + * @returns {Promise} Import statistics including total files, imported count, and new files + * @throws {Error} If statistics calculation fails + * @description Analyzes a directory (or default COMICS_DIRECTORY) and compares + * files against the database to determine import status. This performs a full + * filesystem scan and is slower than getCachedImportStatistics. + * + * @example + * ```graphql + * query { + * getImportStatistics(directoryPath: "/path/to/comics") { + * success + * directory + * stats { + * totalLocalFiles + * alreadyImported + * newFiles + * percentageImported + * } + * } + * } + * ``` + */ + getImportStatistics: async ( + _: any, + { directoryPath }: { directoryPath?: string }, + context: any + ) => { + try { + const broker = context?.broker; + + if (!broker) { + throw new Error("Broker not available in context"); + } + + const result = await broker.call("library.getImportStatistics", { + directoryPath, + }); + return result; + } catch (error) { + console.error("Error fetching import statistics:", error); + throw new Error(`Failed to fetch import statistics: ${error.message}`); + } + }, + /** * Get cached import statistics (fast, real-time) * @async diff --git a/services/api.service.ts b/services/api.service.ts index 60c8133..76f8fa2 100644 --- a/services/api.service.ts +++ b/services/api.service.ts @@ -92,6 +92,7 @@ export default class ApiService extends Service { aliases: { "POST /": "graphql.graphql", "GET /": "graphql.graphql", + "GET /health": "graphql.checkRemoteSchema", }, mappingPolicy: "restrict", bodyParsers: { diff --git a/services/graphql.service.ts b/services/graphql.service.ts index 0e678a1..12db39d 100644 --- a/services/graphql.service.ts +++ b/services/graphql.service.ts @@ -114,6 +114,36 @@ export default { }, actions: { + /** + * Check remote schema health and availability + * @returns Status of remote schema connection with appropriate HTTP status + */ + checkRemoteSchema: { + async handler(ctx: Context) { + const status: any = { + remoteSchemaAvailable: this.remoteSchemaAvailable || false, + remoteUrl: this.settings.metadataGraphqlUrl, + localSchemaOnly: !this.remoteSchemaAvailable, + }; + + if (this.remoteSchemaAvailable && this.schema) { + const queryType = this.schema.getQueryType(); + if (queryType) { + const fields = Object.keys(queryType.getFields()); + status.availableQueryFields = fields; + status.hasWeeklyPullList = fields.includes('getWeeklyPullList'); + } + } + + // Set HTTP status code based on schema stitching status + // 200 = Schema stitching complete (remote available) + // 503 = Service degraded (local only, remote unavailable) + (ctx.meta as any).$statusCode = this.remoteSchemaAvailable ? 200 : 503; + + return status; + }, + }, + /** * Execute GraphQL queries and mutations * @param query - GraphQL query or mutation string @@ -196,11 +226,19 @@ export default { this.logger.info(`Attempting to introspect remote schema at ${this.settings.metadataGraphqlUrl}`); const remoteSchema = await fetchRemoteSchema(this.settings.metadataGraphqlUrl); - this.logger.info("Successfully introspected remote metadata schema"); + this.logger.info("✓ Successfully introspected remote metadata schema"); const remoteQueryType = remoteSchema.getQueryType(); + const remoteMutationType = remoteSchema.getMutationType(); + if (remoteQueryType) { - this.logger.info(`Remote schema Query fields: ${Object.keys(remoteQueryType.getFields()).join(', ')}`); + const remoteQueryFields = Object.keys(remoteQueryType.getFields()); + this.logger.info(`✓ Remote schema has ${remoteQueryFields.length} Query fields: ${remoteQueryFields.join(', ')}`); + } + + if (remoteMutationType) { + const remoteMutationFields = Object.keys(remoteMutationType.getFields()); + this.logger.info(`✓ Remote schema has ${remoteMutationFields.length} Mutation fields: ${remoteMutationFields.join(', ')}`); } this.schema = stitchSchemas({ @@ -212,15 +250,35 @@ export default { }); const stitchedQueryType = this.schema.getQueryType(); + const stitchedMutationType = this.schema.getMutationType(); + if (stitchedQueryType) { - this.logger.info(`Stitched schema Query fields: ${Object.keys(stitchedQueryType.getFields()).join(', ')}`); + const stitchedQueryFields = Object.keys(stitchedQueryType.getFields()); + this.logger.info(`✓ Stitched schema has ${stitchedQueryFields.length} Query fields`); + + // Verify critical remote fields are present + const criticalFields = ['getWeeklyPullList']; + const missingFields = criticalFields.filter(field => !stitchedQueryFields.includes(field)); + if (missingFields.length > 0) { + this.logger.warn(`⚠ Missing expected remote fields: ${missingFields.join(', ')}`); + } } - this.logger.info("Successfully stitched local and remote schemas"); + if (stitchedMutationType) { + const stitchedMutationFields = Object.keys(stitchedMutationType.getFields()); + this.logger.info(`✓ Stitched schema has ${stitchedMutationFields.length} Mutation fields`); + } + + this.logger.info("✓ Successfully stitched local and remote schemas"); + this.remoteSchemaAvailable = true; } catch (remoteError: any) { - this.logger.warn(`Could not connect to remote metadata GraphQL at ${this.settings.metadataGraphqlUrl}: ${remoteError.message}`); - this.logger.warn("Continuing with local schema only"); + this.logger.error(`✗ Failed to connect to remote metadata GraphQL at ${this.settings.metadataGraphqlUrl}`); + this.logger.error(`✗ Error: ${remoteError.message}`); + this.logger.warn("⚠ FALLING BACK TO LOCAL SCHEMA ONLY"); + this.logger.warn("⚠ Remote queries like 'getWeeklyPullList' will NOT be available"); + this.logger.warn(`⚠ To fix: Ensure metadata-graphql service is running at ${this.settings.metadataGraphqlUrl}`); this.schema = localSchema; + this.remoteSchemaAvailable = false; } this.logger.info("GraphQL service started successfully"); diff --git a/services/library.service.ts b/services/library.service.ts index 0e732d7..ac97920 100644 --- a/services/library.service.ts +++ b/services/library.service.ts @@ -623,7 +623,7 @@ export default class ImportService extends Service { console.log( "[GraphQL Import] Triggering metadata resolution..." ); - await this.broker.call("graphql.query", { + await this.broker.call("graphql.graphql", { query: ` mutation ResolveMetadata($comicId: ID!) { resolveMetadata(comicId: $comicId) { diff --git a/utils/import.graphql.utils.ts b/utils/import.graphql.utils.ts index 215ff59..0c5f388 100644 --- a/utils/import.graphql.utils.ts +++ b/utils/import.graphql.utils.ts @@ -135,7 +135,7 @@ export async function importComicViaGraphQL( } try { - const result: any = await broker.call("graphql.query", { + const result: any = await broker.call("graphql.graphql", { query: mutation, variables: { input }, }); @@ -183,7 +183,7 @@ export async function updateSourcedMetadataViaGraphQL( `; try { - const result: any = await broker.call("graphql.query", { + const result: any = await broker.call("graphql.graphql", { query: mutation, variables: { comicId, @@ -229,7 +229,7 @@ export async function resolveMetadataViaGraphQL( `; try { - const result: any = await broker.call("graphql.query", { + const result: any = await broker.call("graphql.graphql", { query: mutation, variables: { comicId }, }); @@ -287,7 +287,7 @@ export async function getComicViaGraphQL( `; try { - const result: any = await broker.call("graphql.query", { + const result: any = await broker.call("graphql.graphql", { query, variables: { id: comicId }, }); @@ -336,7 +336,7 @@ export async function analyzeMetadataConflictsViaGraphQL( `; try { - const result: any = await broker.call("graphql.query", { + const result: any = await broker.call("graphql.graphql", { query, variables: { comicId }, }); @@ -373,7 +373,7 @@ export async function bulkResolveMetadataViaGraphQL( `; try { - const result: any = await broker.call("graphql.query", { + const result: any = await broker.call("graphql.graphql", { query: mutation, variables: { comicIds }, });