🛠 graphql changes

This commit is contained in:
2026-02-24 16:29:48 -05:00
parent cd446a9ca3
commit f7804ee3f0
11 changed files with 3317 additions and 40 deletions

View File

@@ -55,12 +55,120 @@ export default class ApiService extends Service {
mappingPolicy: "all",
logging: true,
},
{
path: "/graphql",
cors: {
origin: "*",
methods: ["GET", "OPTIONS", "POST"],
allowedHeaders: ["*"],
exposedHeaders: [],
credentials: false,
maxAge: 3600,
},
aliases: {
"POST /": async (req: any, res: any) => {
try {
const { query, variables, operationName } = req.body;
const result = await req.$service.broker.call("graphql.query", {
query,
variables,
operationName,
});
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify(result));
} catch (error: any) {
res.statusCode = 500;
res.setHeader("Content-Type", "application/json");
res.end(
JSON.stringify({
errors: [{ message: error.message }],
})
);
}
},
"GET /": async (req: any, res: any) => {
// Support GraphQL Playground or introspection queries via GET
const query = req.$params.query;
const variables = req.$params.variables
? JSON.parse(req.$params.variables)
: undefined;
const operationName = req.$params.operationName;
if (query) {
try {
const result = await req.$service.broker.call("graphql.query", {
query,
variables,
operationName,
});
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify(result));
} catch (error: any) {
res.statusCode = 500;
res.setHeader("Content-Type", "application/json");
res.end(
JSON.stringify({
errors: [{ message: error.message }],
})
);
}
} else {
// Return GraphQL Playground HTML
res.setHeader("Content-Type", "text/html");
res.end(`
<!DOCTYPE html>
<html>
<head>
<title>GraphQL Playground</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/graphql-playground-react/build/static/css/index.css" />
<link rel="shortcut icon" href="https://cdn.jsdelivr.net/npm/graphql-playground-react/build/favicon.png" />
<script src="https://cdn.jsdelivr.net/npm/graphql-playground-react/build/static/js/middleware.js"></script>
</head>
<body>
<div id="root"></div>
<script>
window.addEventListener('load', function (event) {
GraphQLPlayground.init(document.getElementById('root'), {
endpoint: '/graphql',
settings: {
'request.credentials': 'same-origin',
},
})
})
</script>
</body>
</html>
`);
}
},
},
mappingPolicy: "restrict",
bodyParsers: {
json: { strict: false, limit: "1MB" },
},
},
{
path: "/userdata",
cors: {
origin: "*",
methods: ["GET", "OPTIONS"],
allowedHeaders: ["*"],
exposedHeaders: [],
credentials: false,
maxAge: 3600,
},
use: [ApiGateway.serveStatic(path.resolve("./userdata"))],
},
{
path: "/comics",
cors: {
origin: "*",
methods: ["GET", "OPTIONS"],
allowedHeaders: ["*"],
exposedHeaders: [],
credentials: false,
maxAge: 3600,
},
use: [ApiGateway.serveStatic(path.resolve("./comics"))],
},
{

213
services/graphql.service.ts Normal file
View File

@@ -0,0 +1,213 @@
import { Service, ServiceBroker } from "moleculer";
import { ApolloServer } from "@apollo/server";
import { typeDefs } from "../models/graphql/typedef";
import { resolvers } from "../models/graphql/resolvers";
/**
* GraphQL Service
* Provides a GraphQL API for canonical metadata queries and mutations
* Integrates Apollo Server with Moleculer
*/
export default class GraphQLService extends Service {
private apolloServer?: ApolloServer;
public constructor(broker: ServiceBroker) {
super(broker);
this.parseServiceSchema({
name: "graphql",
settings: {
// GraphQL endpoint path
path: "/graphql",
},
actions: {
/**
* Execute a GraphQL query
*/
query: {
params: {
query: "string",
variables: { type: "object", optional: true },
operationName: { type: "string", optional: true },
},
async handler(ctx: any) {
try {
if (!this.apolloServer) {
throw new Error("Apollo Server not initialized");
}
const { query, variables, operationName } = ctx.params;
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 query error:", error);
throw error;
}
},
},
/**
* Get GraphQL schema
*/
getSchema: {
async handler() {
return {
typeDefs: typeDefs.loc?.source.body || "",
};
},
},
},
methods: {
/**
* Initialize Apollo Server
*/
async initApolloServer() {
this.logger.info("Initializing Apollo Server...");
this.apolloServer = new ApolloServer({
typeDefs,
resolvers,
introspection: true, // Enable GraphQL Playground in development
formatError: (error) => {
this.logger.error("GraphQL Error:", error);
return {
message: error.message,
locations: error.locations,
path: error.path,
extensions: {
code: error.extensions?.code,
},
};
},
});
await this.apolloServer.start();
this.logger.info("Apollo Server started successfully");
},
/**
* Stop Apollo Server
*/
async stopApolloServer() {
if (this.apolloServer) {
this.logger.info("Stopping Apollo Server...");
await this.apolloServer.stop();
this.apolloServer = undefined;
this.logger.info("Apollo Server stopped");
}
},
},
events: {
/**
* Trigger metadata resolution when new metadata is imported
*/
"metadata.imported": {
async handler(ctx: any) {
const { comicId, source } = ctx.params;
this.logger.info(
`Metadata imported for comic ${comicId} from ${source}`
);
// Optionally trigger auto-resolution if enabled
try {
const UserPreferences = require("../models/userpreferences.model").default;
const preferences = await UserPreferences.findOne({
userId: "default",
});
if (
preferences?.autoMerge?.enabled &&
preferences?.autoMerge?.onMetadataUpdate
) {
this.logger.info(
`Auto-resolving metadata for comic ${comicId}`
);
await this.broker.call("graphql.query", {
query: `
mutation ResolveMetadata($comicId: ID!) {
resolveMetadata(comicId: $comicId) {
id
}
}
`,
variables: { comicId },
});
}
} catch (error) {
this.logger.error("Error in auto-resolution:", error);
}
},
},
/**
* Trigger metadata resolution when comic is imported
*/
"comic.imported": {
async handler(ctx: any) {
const { comicId } = ctx.params;
this.logger.info(`Comic imported: ${comicId}`);
// Optionally trigger auto-resolution if enabled
try {
const UserPreferences = require("../models/userpreferences.model").default;
const preferences = await UserPreferences.findOne({
userId: "default",
});
if (
preferences?.autoMerge?.enabled &&
preferences?.autoMerge?.onImport
) {
this.logger.info(
`Auto-resolving metadata for newly imported comic ${comicId}`
);
await this.broker.call("graphql.query", {
query: `
mutation ResolveMetadata($comicId: ID!) {
resolveMetadata(comicId: $comicId) {
id
}
}
`,
variables: { comicId },
});
}
} catch (error) {
this.logger.error("Error in auto-resolution on import:", error);
}
},
},
},
started: async function (this: any) {
await this.initApolloServer();
},
stopped: async function (this: any) {
await this.stopApolloServer();
},
});
}
}

View File

@@ -58,6 +58,7 @@ import klaw from "klaw";
import path from "path";
import { COMICS_DIRECTORY, USERDATA_DIRECTORY } from "../constants/directories";
import AirDCPPSocket from "../shared/airdcpp.socket";
import { importComicViaGraphQL } from "../utils/import.graphql.utils";
console.log(`MONGO -> ${process.env.MONGO_URI}`);
export default class ImportService extends Service {
@@ -256,61 +257,119 @@ export default class ImportService extends Service {
sourcedMetadata: {
comicvine?: any;
locg?: {};
comicInfo?: any;
metron?: any;
gcd?: any;
};
inferredMetadata: {
issue: Object;
};
rawFileDetails: {
name: string;
filePath: string;
fileSize?: number;
extension?: string;
mimeType?: string;
containedIn?: string;
cover?: any;
};
wanted: {
wanted?: {
issues: [];
volume: { id: number };
source: string;
markEntireVolumeWanted: Boolean;
};
acquisition: {
directconnect: {
acquisition?: {
source?: {
wanted?: boolean;
name?: string;
};
directconnect?: {
downloads: [];
};
};
importStatus?: {
isImported: boolean;
tagged: boolean;
matchedResult?: {
score: string;
};
};
};
}>
) {
try {
console.log(
"[GraphQL Import] Processing import via GraphQL..."
);
console.log(
JSON.stringify(ctx.params.payload, null, 4)
);
const { payload } = ctx.params;
const { wanted } = payload;
console.log("Saving to Mongo...");
// Use GraphQL import for new comics
if (
!wanted ||
!wanted.volume ||
!wanted.volume.id
) {
console.log(
"No valid identifier for upsert. Attempting to create a new document with minimal data..."
"[GraphQL Import] No valid identifier - creating new comic via GraphQL"
);
const newDocument = new Comic(payload); // Using the entire payload for the new document
await newDocument.save();
return {
success: true,
message:
"New document created due to lack of valid identifiers.",
data: newDocument,
};
// Import via GraphQL
const result = await importComicViaGraphQL(
this.broker,
{
filePath: payload.rawFileDetails.filePath,
fileSize: payload.rawFileDetails.fileSize,
rawFileDetails: payload.rawFileDetails,
inferredMetadata: payload.inferredMetadata,
sourcedMetadata: payload.sourcedMetadata,
wanted: payload.wanted ? {
...payload.wanted,
markEntireVolumeWanted: Boolean(payload.wanted.markEntireVolumeWanted)
} : undefined,
acquisition: payload.acquisition,
}
);
if (result.success) {
console.log(
`[GraphQL Import] Comic imported successfully: ${result.comic.id}`
);
console.log(
`[GraphQL Import] Canonical metadata resolved: ${result.canonicalMetadataResolved}`
);
return {
success: true,
message: result.message,
data: result.comic,
};
} else {
console.log(
`[GraphQL Import] Import returned success=false: ${result.message}`
);
return {
success: false,
message: result.message,
data: result.comic,
};
}
}
// For comics with wanted.volume.id, use upsert logic
console.log(
"[GraphQL Import] Comic has wanted.volume.id - using upsert logic"
);
let condition = {
"wanted.volume.id": wanted.volume.id,
};
let update: any = {
// Using 'any' to bypass strict type checks; alternatively, define a more accurate type
$set: {
rawFileDetails: payload.rawFileDetails,
inferredMetadata: payload.inferredMetadata,
@@ -340,18 +399,45 @@ export default class ImportService extends Service {
update,
options
);
console.log(
"Operation completed. Document updated or inserted:",
result
"[GraphQL Import] Document upserted:",
result._id
);
// Trigger canonical metadata resolution via GraphQL
try {
console.log(
"[GraphQL Import] Triggering metadata resolution..."
);
await this.broker.call("graphql.query", {
query: `
mutation ResolveMetadata($comicId: ID!) {
resolveMetadata(comicId: $comicId) {
id
}
}
`,
variables: { comicId: result._id.toString() },
});
console.log(
"[GraphQL Import] Metadata resolution triggered"
);
} catch (resolveError) {
console.error(
"[GraphQL Import] Error resolving metadata:",
resolveError
);
// Don't fail the import if resolution fails
}
return {
success: true,
message: "Document successfully upserted.",
data: result,
};
} catch (error) {
console.log(error);
console.error("[GraphQL Import] Error:", error);
throw new Errors.MoleculerError(
"Operation failed.",
500