🛠 graphql changes
This commit is contained in:
@@ -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
213
services/graphql.service.ts
Normal 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();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user