📚 Import stats hardening

This commit is contained in:
2026-03-05 15:05:12 -05:00
parent 71267ecc7e
commit 8138e0fe4f
5 changed files with 836 additions and 74 deletions

View File

@@ -766,6 +766,56 @@ export const resolvers = {
throw new Error(`Failed to fetch job result statistics: ${error.message}`);
}
},
/**
* Get active import session (if any)
* @async
* @function getActiveImportSession
* @param {any} _ - Parent resolver (unused)
* @param {Object} args - Query arguments (none)
* @param {Object} context - GraphQL context with broker
* @returns {Promise<Object|null>} Active import session or null
* @throws {Error} If import state service is unavailable
* @description Retrieves the currently active import session (if any).
* Useful for checking if an import is in progress before starting a new one.
*
* @example
* ```graphql
* query {
* getActiveImportSession {
* sessionId
* type
* status
* startedAt
* stats {
* totalFiles
* filesProcessed
* filesSucceeded
* filesFailed
* }
* }
* }
* ```
*/
getActiveImportSession: async (
_: any,
args: {},
context: any
) => {
try {
const broker = context?.broker;
if (!broker) {
throw new Error("Broker not available in context");
}
const session = await broker.call("importstate.getActiveSession");
return session;
} catch (error) {
console.error("Error fetching active import session:", error);
throw new Error(`Failed to fetch active import session: ${error.message}`);
}
},
},
Mutation: {
@@ -1373,12 +1423,143 @@ export const resolvers = {
throw new Error(`Failed to update sourced metadata: ${error.message}`);
}
},
/**
* Start a new full import of the comics directory
* @async
* @function startNewImport
* @param {any} _ - Parent resolver (unused)
* @param {Object} args - Mutation arguments
* @param {string} args.sessionId - Session ID for tracking this import batch
* @param {Object} context - GraphQL context with broker
* @returns {Promise<Object>} Import job result with success status and jobs queued count
* @throws {Error} If import service is unavailable or import fails
* @description Starts a full import of all comics in the comics directory.
* Scans the entire directory and queues jobs for all comic files that haven't
* been imported yet. Checks for active import sessions to prevent race conditions.
*
* @example
* ```graphql
* mutation {
* startNewImport(sessionId: "import-2024-01-01") {
* success
* message
* jobsQueued
* }
* }
* ```
*/
startNewImport: async (
_: any,
{ sessionId }: { sessionId: string },
context: any
) => {
try {
const broker = context?.broker;
if (!broker) {
throw new Error("Broker not available in context");
}
// Check for active import sessions (race condition prevention)
const activeSession = await broker.call("importstate.getActiveSession");
if (activeSession) {
throw new Error(
`Cannot start new import: Another import session "${activeSession.sessionId}" is already active (${activeSession.type}). Please wait for it to complete.`
);
}
// Call the library service to start new import
await broker.call("library.newImport", {
sessionId,
});
return {
success: true,
message: "New import started successfully",
jobsQueued: 0, // The actual count is tracked asynchronously
};
} catch (error) {
console.error("Error starting new import:", error);
throw new Error(`Failed to start new import: ${error.message}`);
}
},
/**
* Start an incremental import (only new files)
* @async
* @function startIncrementalImport
* @param {any} _ - Parent resolver (unused)
* @param {Object} args - Mutation arguments
* @param {string} args.sessionId - Session ID for tracking this import batch
* @param {string} [args.directoryPath] - Optional directory path to scan (defaults to COMICS_DIRECTORY)
* @param {Object} context - GraphQL context with broker
* @returns {Promise<Object>} Incremental import result with statistics
* @throws {Error} If import service is unavailable or import fails
* @description Starts an incremental import that only processes new files
* not already in the database. More efficient than full import for large libraries.
* Checks for active import sessions to prevent race conditions.
*
* @example
* ```graphql
* mutation {
* startIncrementalImport(
* sessionId: "incremental-2024-01-01"
* directoryPath: "/path/to/comics"
* ) {
* success
* message
* stats {
* total
* alreadyImported
* newFiles
* queued
* }
* }
* }
* ```
*/
startIncrementalImport: async (
_: any,
{
sessionId,
directoryPath,
}: { sessionId: string; directoryPath?: string },
context: any
) => {
try {
const broker = context?.broker;
if (!broker) {
throw new Error("Broker not available in context");
}
// Check for active import sessions (race condition prevention)
const activeSession = await broker.call("importstate.getActiveSession");
if (activeSession) {
throw new Error(
`Cannot start incremental import: Another import session "${activeSession.sessionId}" is already active (${activeSession.type}). Please wait for it to complete.`
);
}
// Call the library service to start incremental import
const result = await broker.call("library.incrementalImport", {
sessionId,
directoryPath,
});
return result;
} catch (error) {
console.error("Error starting incremental import:", error);
throw new Error(`Failed to start incremental import: ${error.message}`);
}
},
},
/**
* Field resolvers for Comic type
* @description Custom field resolvers for transforming Comic data
*/
* Field resolvers for Comic type
* @description Custom field resolvers for transforming Comic data
*/
Comic: {
/**
* Resolve Comic ID field

View File

@@ -353,6 +353,9 @@ export const typeDefs = gql`
# Get job result statistics grouped by session
getJobResultStatistics: [JobResultStatistics!]!
# Get active import session (if any)
getActiveImportSession: ImportSession
}
# Mutations
@@ -780,4 +783,23 @@ export const typeDefs = gql`
failedJobs: Int!
earliestTimestamp: String!
}
# Import session information
type ImportSession {
sessionId: String!
type: String!
status: String!
startedAt: String!
completedAt: String
stats: ImportSessionStats!
directoryPath: String
}
type ImportSessionStats {
totalFiles: Int!
filesQueued: Int!
filesProcessed: Int!
filesSucceeded: Int!
filesFailed: Int!
}
`;