🪢Added resolvers for lib, dashboard endpoints

This commit is contained in:
2026-03-04 21:49:38 -05:00
parent 8c224bad68
commit 22cbdcd468
9 changed files with 1846 additions and 10 deletions

View File

@@ -0,0 +1,139 @@
/**
* ComicDetailContainer - GraphQL Version
*
* This file should replace the existing ComicDetailContainer.tsx
* Location: src/client/components/ComicDetail/ComicDetailContainer.tsx
*
* Key changes from REST version:
* 1. Uses executeGraphQLQuery instead of axios directly
* 2. Parses JSON strings from sourcedMetadata
* 3. Maps GraphQL 'id' to REST '_id' for backward compatibility
* 4. Better error and loading states
*/
import React, { ReactElement } from "react";
import { useParams } from "react-router-dom";
import { ComicDetail } from "../ComicDetail/ComicDetail";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { executeGraphQLQuery, transformComicToRestFormat } from "../../services/api/GraphQLApi";
import {
GET_COMIC_DETAIL_QUERY,
ComicDetailQueryResponse
} from "../../graphql/queries/comicDetail";
export const ComicDetailContainer = (): ReactElement | null => {
const { comicObjectId } = useParams<{ comicObjectId: string }>();
const queryClient = useQueryClient();
const {
data: comicBookDetailData,
isLoading,
isError,
error,
} = useQuery({
queryKey: ["comicBookMetadata", comicObjectId],
queryFn: async () => {
// Execute GraphQL query
const result = await executeGraphQLQuery<ComicDetailQueryResponse>(
GET_COMIC_DETAIL_QUERY,
{ id: comicObjectId }
);
// Transform to REST format for backward compatibility
const transformedComic = transformComicToRestFormat(result.comic);
// Return in the format expected by ComicDetail component
return {
data: transformedComic,
};
},
enabled: !!comicObjectId, // Only run query if we have an ID
staleTime: 5 * 60 * 1000, // Consider data fresh for 5 minutes
retry: 2, // Retry failed requests twice
});
if (isError) {
return (
<div className="mx-auto max-w-screen-xl px-4 py-4">
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
<strong className="font-bold">Error loading comic: </strong>
<span className="block sm:inline">
{error instanceof Error ? error.message : 'Unknown error'}
</span>
</div>
</div>
);
}
if (isLoading) {
return (
<div className="mx-auto max-w-screen-xl px-4 py-4">
<div className="flex items-center justify-center">
<div className="text-gray-500 dark:text-gray-400">
Loading comic details...
</div>
</div>
</div>
);
}
return (
comicBookDetailData?.data && (
<ComicDetail
data={comicBookDetailData.data}
queryClient={queryClient}
comicObjectId={comicObjectId}
/>
)
);
};
/**
* Alternative implementation with feature flag for gradual rollout
* Uncomment this version if you want to toggle between REST and GraphQL
*/
/*
export const ComicDetailContainer = (): ReactElement | null => {
const { comicObjectId } = useParams<{ comicObjectId: string }>();
const queryClient = useQueryClient();
// Feature flag to toggle between REST and GraphQL
const USE_GRAPHQL = import.meta.env.VITE_USE_GRAPHQL === 'true';
const {
data: comicBookDetailData,
isLoading,
isError,
error,
} = useQuery({
queryKey: ["comicBookMetadata", comicObjectId],
queryFn: async () => {
if (USE_GRAPHQL) {
// GraphQL implementation
const result = await executeGraphQLQuery<ComicDetailQueryResponse>(
GET_COMIC_DETAIL_QUERY,
{ id: comicObjectId }
);
const transformedComic = transformComicToRestFormat(result.comic);
return {
data: transformedComic,
};
} else {
// REST implementation (fallback)
const response = await axios({
url: `${LIBRARY_SERVICE_BASE_URI}/getComicBookById`,
method: "POST",
data: { id: comicObjectId },
});
return response;
}
},
enabled: !!comicObjectId,
});
// ... rest of the component remains the same
};
*/

View File

@@ -0,0 +1,248 @@
/**
* GraphQL query to fetch complete comic detail data
*
* This file should be placed in the frontend project at:
* src/client/graphql/queries/comicDetail.ts
*
* Matches the data structure expected by ComicDetail component
*/
export const GET_COMIC_DETAIL_QUERY = `
query GetComicDetail($id: ID!) {
comic(id: $id) {
id
# Raw file information
rawFileDetails {
name
filePath
fileSize
extension
mimeType
containedIn
pageCount
archive {
uncompressed
expandedPath
}
cover {
filePath
stats
}
}
# Inferred metadata from filename parsing
inferredMetadata {
issue {
name
number
year
subtitle
}
}
# Sourced metadata from various providers
sourcedMetadata {
comicInfo
comicvine
metron
gcd
locg {
name
publisher
url
cover
description
price
rating
pulls
potw
}
}
# Import status
importStatus {
isImported
tagged
matchedResult {
score
}
}
# Acquisition/download information
acquisition {
source {
wanted
name
}
directconnect {
downloads {
bundleId
name
size
}
}
torrent {
infoHash
name
announce
}
}
# Timestamps
createdAt
updatedAt
}
}
`;
/**
* TypeScript type for the query response
* Generated from GraphQL schema
*/
export interface ComicDetailQueryResponse {
comic: {
id: string;
rawFileDetails?: {
name?: string;
filePath?: string;
fileSize?: number;
extension?: string;
mimeType?: string;
containedIn?: string;
pageCount?: number;
archive?: {
uncompressed?: boolean;
expandedPath?: string;
};
cover?: {
filePath?: string;
stats?: any;
};
};
inferredMetadata?: {
issue?: {
name?: string;
number?: number;
year?: string;
subtitle?: string;
};
};
sourcedMetadata?: {
comicInfo?: string; // JSON string - needs parsing
comicvine?: string; // JSON string - needs parsing
metron?: string; // JSON string - needs parsing
gcd?: string; // JSON string - needs parsing
locg?: {
name?: string;
publisher?: string;
url?: string;
cover?: string;
description?: string;
price?: string;
rating?: number;
pulls?: number;
potw?: number;
};
};
importStatus?: {
isImported?: boolean;
tagged?: boolean;
matchedResult?: {
score?: string;
};
};
acquisition?: {
source?: {
wanted?: boolean;
name?: string;
};
directconnect?: {
downloads?: Array<{
bundleId?: number;
name?: string;
size?: string;
}>;
};
torrent?: Array<{
infoHash?: string;
name?: string;
announce?: string[];
}>;
};
createdAt?: string;
updatedAt?: string;
};
}
/**
* Minimal query for basic comic information
* Use this when you only need basic details
*/
export const GET_COMIC_BASIC_QUERY = `
query GetComicBasic($id: ID!) {
comic(id: $id) {
id
rawFileDetails {
name
filePath
fileSize
pageCount
}
inferredMetadata {
issue {
name
number
year
}
}
}
}
`;
/**
* Query for comic metadata only (no file details)
* Use this when you only need metadata
*/
export const GET_COMIC_METADATA_QUERY = `
query GetComicMetadata($id: ID!) {
comic(id: $id) {
id
sourcedMetadata {
comicInfo
comicvine
metron
gcd
locg {
name
publisher
description
rating
}
}
canonicalMetadata {
title {
value
provenance {
source
confidence
}
}
series {
value
provenance {
source
confidence
}
}
publisher {
value
provenance {
source
confidence
}
}
}
}
}
`;

View File

@@ -0,0 +1,260 @@
/**
* GraphQL queries for library operations
* Examples for getComicBooks, getComicBookGroups, getLibraryStatistics, and searchIssue
*/
/**
* Query to get comic books with pagination and filtering
*/
export const GET_COMIC_BOOKS = `
query GetComicBooks($paginationOptions: PaginationOptionsInput!, $predicate: PredicateInput) {
getComicBooks(paginationOptions: $paginationOptions, predicate: $predicate) {
docs {
id
canonicalMetadata {
title {
value
provenance {
source
confidence
}
}
series {
value
}
issueNumber {
value
}
publisher {
value
}
coverImage {
value
}
}
rawFileDetails {
name
filePath
fileSize
extension
}
createdAt
updatedAt
}
totalDocs
limit
page
totalPages
hasNextPage
hasPrevPage
nextPage
prevPage
pagingCounter
}
}
`;
/**
* Query to get comic book groups (volumes)
*/
export const GET_COMIC_BOOK_GROUPS = `
query GetComicBookGroups {
getComicBookGroups {
id
volumes {
id
name
count_of_issues
publisher {
id
name
}
start_year
image {
medium_url
thumb_url
}
description
site_detail_url
}
}
}
`;
/**
* Query to get library statistics
*/
export const GET_LIBRARY_STATISTICS = `
query GetLibraryStatistics {
getLibraryStatistics {
totalDocuments
comicDirectorySize {
totalSize
totalSizeInMB
totalSizeInGB
fileCount
}
statistics {
fileTypes {
id
data
}
publisherWithMostComicsInLibrary {
id
count
}
}
}
}
`;
/**
* Example usage with variables for getComicBooks
*/
export const exampleGetComicBooksVariables = {
paginationOptions: {
page: 1,
limit: 10,
sort: "-createdAt", // Sort by creation date, descending
lean: false,
pagination: true,
},
predicate: {
// Optional: Add filters here
// Example: { "canonicalMetadata.publisher.value": "Marvel" }
},
};
/**
* Example: Get first page of comics
*/
export const exampleGetFirstPage = {
query: GET_COMIC_BOOKS,
variables: {
paginationOptions: {
page: 1,
limit: 20,
sort: "-createdAt",
},
},
};
/**
* Example: Get comics with specific filters
*/
export const exampleGetFilteredComics = {
query: GET_COMIC_BOOKS,
variables: {
paginationOptions: {
page: 1,
limit: 10,
},
predicate: {
"importStatus.isImported": true,
},
},
};
/**
* Query to search issues using Elasticsearch
*/
export const SEARCH_ISSUE = `
query SearchIssue($query: SearchIssueQueryInput, $pagination: SearchPaginationInput, $type: SearchType!) {
searchIssue(query: $query, pagination: $pagination, type: $type) {
hits {
total {
value
relation
}
max_score
hits {
_index
_id
_score
_source {
id
canonicalMetadata {
title {
value
}
series {
value
}
issueNumber {
value
}
publisher {
value
}
}
rawFileDetails {
name
filePath
}
}
}
}
took
timed_out
}
}
`;
/**
* Example: Search all comics
*/
export const exampleSearchAll = {
query: SEARCH_ISSUE,
variables: {
type: "all",
pagination: {
size: 10,
from: 0,
},
},
};
/**
* Example: Search by volume name
*/
export const exampleSearchByVolumeName = {
query: SEARCH_ISSUE,
variables: {
query: {
volumeName: "Spider-Man",
},
type: "volumeName",
pagination: {
size: 20,
from: 0,
},
},
};
/**
* Example: Search wanted comics
*/
export const exampleSearchWanted = {
query: SEARCH_ISSUE,
variables: {
type: "wanted",
pagination: {
size: 50,
from: 0,
},
},
};
/**
* Example: Search volumes
*/
export const exampleSearchVolumes = {
query: SEARCH_ISSUE,
variables: {
type: "volumes",
pagination: {
size: 10,
from: 0,
},
},
};

View File

@@ -0,0 +1,165 @@
/**
* GraphQL API Client Utility
*
* This file should be placed in the frontend project at:
* src/client/services/api/GraphQLApi.ts
*
* Simple wrapper around axios for executing GraphQL queries and mutations
* No additional dependencies needed (no Apollo Client)
* Works seamlessly with React Query
*/
import axios from 'axios';
// Update this to match your frontend constants file
// import { LIBRARY_SERVICE_BASE_URI } from '../../constants/endpoints';
const LIBRARY_SERVICE_BASE_URI = process.env.REACT_APP_LIBRARY_SERVICE_BASE_URI || 'http://localhost:3000/api/library';
/**
* Execute a GraphQL query against the threetwo-core-service GraphQL endpoint
*
* @param query - GraphQL query string
* @param variables - Query variables
* @returns Promise with query result data
*
* @example
* ```typescript
* const result = await executeGraphQLQuery<ComicDetailQueryResponse>(
* GET_COMIC_DETAIL_QUERY,
* { id: 'comic-id-123' }
* );
* console.log(result.comic.rawFileDetails.name);
* ```
*/
export const executeGraphQLQuery = async <T = any>(
query: string,
variables?: Record<string, any>
): Promise<T> => {
try {
const response = await axios.post(
`${LIBRARY_SERVICE_BASE_URI}/graphql`,
{
query,
variables,
},
{
headers: {
'Content-Type': 'application/json',
},
}
);
// GraphQL can return partial data with errors
if (response.data.errors) {
console.error('GraphQL errors:', response.data.errors);
throw new Error(
`GraphQL errors: ${response.data.errors.map((e: any) => e.message).join(', ')}`
);
}
return response.data.data;
} catch (error) {
console.error('GraphQL query failed:', error);
throw error;
}
};
/**
* Execute a GraphQL mutation against the threetwo-core-service GraphQL endpoint
*
* @param mutation - GraphQL mutation string
* @param variables - Mutation variables
* @returns Promise with mutation result data
*
* @example
* ```typescript
* const result = await executeGraphQLMutation<{ setMetadataField: Comic }>(
* SET_METADATA_FIELD_MUTATION,
* { comicId: '123', field: 'title', value: 'New Title' }
* );
* console.log(result.setMetadataField.canonicalMetadata.title);
* ```
*/
export const executeGraphQLMutation = async <T = any>(
mutation: string,
variables?: Record<string, any>
): Promise<T> => {
// Mutations use the same endpoint as queries
return executeGraphQLQuery<T>(mutation, variables);
};
/**
* Helper function to parse JSON strings from sourcedMetadata
* GraphQL returns these fields as JSON strings that need parsing
*
* @param sourcedMetadata - The sourcedMetadata object from GraphQL response
* @returns Parsed sourcedMetadata with JSON fields converted to objects
*
* @example
* ```typescript
* const comic = result.comic;
* comic.sourcedMetadata = parseSourcedMetadata(comic.sourcedMetadata);
* // Now comic.sourcedMetadata.comicInfo is an object, not a string
* ```
*/
export const parseSourcedMetadata = (sourcedMetadata: any) => {
if (!sourcedMetadata) return sourcedMetadata;
const parsed = { ...sourcedMetadata };
// Parse JSON strings
if (parsed.comicInfo && typeof parsed.comicInfo === 'string') {
try {
parsed.comicInfo = JSON.parse(parsed.comicInfo);
} catch (e) {
console.warn('Failed to parse comicInfo:', e);
parsed.comicInfo = {};
}
}
if (parsed.comicvine && typeof parsed.comicvine === 'string') {
try {
parsed.comicvine = JSON.parse(parsed.comicvine);
} catch (e) {
console.warn('Failed to parse comicvine:', e);
parsed.comicvine = {};
}
}
if (parsed.metron && typeof parsed.metron === 'string') {
try {
parsed.metron = JSON.parse(parsed.metron);
} catch (e) {
console.warn('Failed to parse metron:', e);
parsed.metron = {};
}
}
if (parsed.gcd && typeof parsed.gcd === 'string') {
try {
parsed.gcd = JSON.parse(parsed.gcd);
} catch (e) {
console.warn('Failed to parse gcd:', e);
parsed.gcd = {};
}
}
return parsed;
};
/**
* Helper function to transform GraphQL comic response to REST format
* Ensures backward compatibility with existing components
*
* @param comic - Comic object from GraphQL response
* @returns Comic object in REST format with _id field
*/
export const transformComicToRestFormat = (comic: any) => {
if (!comic) return null;
return {
_id: comic.id,
...comic,
sourcedMetadata: parseSourcedMetadata(comic.sourcedMetadata),
};
};

View File

@@ -0,0 +1,87 @@
#!/bin/bash
# Test GraphQL Endpoint Script
# This script tests the GraphQL endpoint with various queries
GRAPHQL_URL="http://localhost:3000/graphql"
echo "🧪 Testing GraphQL Endpoint: $GRAPHQL_URL"
echo "================================================"
echo ""
# Test 1: List Comics
echo "📚 Test 1: List Comics (first 5)"
echo "--------------------------------"
curl -s -X POST $GRAPHQL_URL \
-H "Content-Type: application/json" \
-d '{
"query": "query { comics(limit: 5) { comics { id rawFileDetails { name pageCount } } totalCount } }"
}' | jq '.'
echo ""
echo ""
# Test 2: Get Single Comic (you need to replace COMIC_ID)
echo "📖 Test 2: Get Single Comic"
echo "--------------------------------"
echo "⚠️ Replace COMIC_ID with an actual comic ID from your database"
read -p "Enter Comic ID (or press Enter to skip): " COMIC_ID
if [ ! -z "$COMIC_ID" ]; then
curl -s -X POST $GRAPHQL_URL \
-H "Content-Type: application/json" \
-d "{
\"query\": \"query GetComic(\$id: ID!) { comic(id: \$id) { id rawFileDetails { name filePath fileSize pageCount } sourcedMetadata { locg { name publisher rating } } } }\",
\"variables\": { \"id\": \"$COMIC_ID\" }
}" | jq '.'
else
echo "Skipped"
fi
echo ""
echo ""
# Test 3: Get User Preferences
echo "⚙️ Test 3: Get User Preferences"
echo "--------------------------------"
curl -s -X POST $GRAPHQL_URL \
-H "Content-Type: application/json" \
-d '{
"query": "query { userPreferences(userId: \"default\") { id userId conflictResolution minConfidenceThreshold autoMerge { enabled onImport onMetadataUpdate } } }"
}' | jq '.'
echo ""
echo ""
# Test 4: Search Comics
echo "🔍 Test 4: Search Comics"
echo "--------------------------------"
read -p "Enter search term (or press Enter to skip): " SEARCH_TERM
if [ ! -z "$SEARCH_TERM" ]; then
curl -s -X POST $GRAPHQL_URL \
-H "Content-Type: application/json" \
-d "{
\"query\": \"query SearchComics(\$search: String) { comics(search: \$search, limit: 10) { comics { id rawFileDetails { name } } totalCount } }\",
\"variables\": { \"search\": \"$SEARCH_TERM\" }
}" | jq '.'
else
echo "Skipped"
fi
echo ""
echo ""
# Test 5: GraphQL Introspection (get schema info)
echo "🔬 Test 5: Introspection - Available Queries"
echo "--------------------------------"
curl -s -X POST $GRAPHQL_URL \
-H "Content-Type: application/json" \
-d '{
"query": "{ __schema { queryType { fields { name description } } } }"
}' | jq '.data.__schema.queryType.fields[] | {name, description}'
echo ""
echo ""
echo "✅ GraphQL endpoint tests complete!"
echo ""
echo "💡 Tips:"
echo " - Open http://localhost:3000/graphql in your browser for GraphQL Playground"
echo " - Use 'jq' for better JSON formatting (install with: apt-get install jq)"
echo " - Check the docs at: docs/FRONTEND_GRAPHQL_INTEGRATION.md"