🪢Added resolvers for lib, dashboard endpoints
This commit is contained in:
139
examples/frontend/components/ComicDetailContainer.tsx
Normal file
139
examples/frontend/components/ComicDetailContainer.tsx
Normal 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
|
||||
};
|
||||
*/
|
||||
248
examples/frontend/graphql-queries/comicDetail.ts
Normal file
248
examples/frontend/graphql-queries/comicDetail.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
260
examples/frontend/graphql-queries/libraryQueries.ts
Normal file
260
examples/frontend/graphql-queries/libraryQueries.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
};
|
||||
165
examples/frontend/services/GraphQLApi.ts
Normal file
165
examples/frontend/services/GraphQLApi.ts
Normal 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),
|
||||
};
|
||||
};
|
||||
87
examples/test-graphql-endpoint.sh
Executable file
87
examples/test-graphql-endpoint.sh
Executable 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"
|
||||
Reference in New Issue
Block a user