removed useless files

This commit is contained in:
2026-03-05 21:49:32 -05:00
parent c7d3d46bcf
commit 42c427c7ea
9 changed files with 0 additions and 2253 deletions

View File

@@ -1,470 +0,0 @@
# GraphQL Service Documentation
## Overview
The GraphQL service provides a unified API for querying and mutating comic metadata. It supports schema stitching with a remote metadata service, comprehensive error handling, validation, caching, and monitoring.
## Architecture
### Components
1. **Main Service** ([`services/graphql.service.ts`](../services/graphql.service.ts))
- Core GraphQL execution engine
- Schema initialization and stitching
- Health monitoring
- Event handling for auto-resolution
2. **Schema Utilities** ([`utils/graphql.schema.utils.ts`](../utils/graphql.schema.utils.ts))
- Remote schema fetching with retry logic
- Schema validation
- Remote executor creation
3. **Validation Utilities** ([`utils/graphql.validation.utils.ts`](../utils/graphql.validation.utils.ts))
- Input validation
- Parameter sanitization
- Type checking
4. **Error Handling** ([`utils/graphql.error.utils.ts`](../utils/graphql.error.utils.ts))
- Standardized error codes
- Error formatting and sanitization
- Error logging
5. **Configuration** ([`config/graphql.config.ts`](../config/graphql.config.ts))
- Centralized configuration management
- Environment variable overrides
## Features
### 1. Schema Stitching
The service combines a local schema with a remote metadata schema:
```typescript
// Local schema: Comic library operations
// Remote schema: Metadata provider operations (ComicVine, Metron, etc.)
```
**Benefits:**
- Single GraphQL endpoint for all operations
- Transparent federation of multiple data sources
- Graceful degradation if remote service is unavailable
### 2. Error Handling
Comprehensive error handling with standardized error codes:
```typescript
enum GraphQLErrorCode {
BAD_REQUEST = "BAD_REQUEST",
VALIDATION_ERROR = "VALIDATION_ERROR",
INTERNAL_SERVER_ERROR = "INTERNAL_SERVER_ERROR",
TIMEOUT = "TIMEOUT",
REMOTE_SCHEMA_ERROR = "REMOTE_SCHEMA_ERROR",
// ... more codes
}
```
**Features:**
- Automatic error classification
- Safe error sanitization for clients
- Detailed logging for debugging
- Stack traces in development mode only
### 3. Retry Logic
Automatic retry for transient failures:
```typescript
{
retries: 3,
retryDelay: 2000, // Exponential backoff
timeout: 10000
}
```
**Retryable Errors:**
- Network errors (ECONNREFUSED, ENOTFOUND)
- Timeout errors
- Service unavailable errors
### 4. Caching
Remote schema caching to reduce latency:
```typescript
{
cacheEnabled: true,
cacheTTL: 3600 // 1 hour
}
```
**Benefits:**
- Faster query execution
- Reduced load on remote service
- Configurable TTL
### 5. Health Monitoring
Periodic health checks for remote schema:
```typescript
{
healthCheck: {
enabled: true,
interval: 60000 // 1 minute
}
}
```
**Health Status:**
```json
{
"healthy": true,
"localSchema": true,
"remoteSchema": true,
"lastCheck": "2026-03-05T15:00:00.000Z",
"remoteSchemaUrl": "http://localhost:3080/metadata-graphql"
}
```
### 6. Performance Monitoring
Query performance tracking:
```typescript
{
logging: {
logPerformance: true,
slowQueryThreshold: 1000 // Log queries > 1s
}
}
```
### 7. Input Validation
Comprehensive input validation:
- Pagination parameters (page, limit, offset)
- ID format validation (MongoDB ObjectId)
- Search query length limits
- File path sanitization
- JSON validation
### 8. Timeout Protection
Query execution timeouts:
```typescript
{
execution: {
timeout: 30000 // 30 seconds
}
}
```
## Configuration
### Environment Variables
```bash
# Remote schema
METADATA_GRAPHQL_URL=http://localhost:3080/metadata-graphql
GRAPHQL_REMOTE_TIMEOUT=10000
GRAPHQL_REMOTE_RETRIES=3
# Execution
GRAPHQL_EXECUTION_TIMEOUT=30000
GRAPHQL_MAX_QUERY_DEPTH=10
# Caching
GRAPHQL_CACHE_ENABLED=true
GRAPHQL_CACHE_TTL=3600
# Environment
NODE_ENV=development
```
### Default Configuration
See [`config/graphql.config.ts`](../config/graphql.config.ts) for all configuration options.
## API Actions
### 1. Execute GraphQL Query
```typescript
broker.call("graphql.graphql", {
query: "query { comic(id: \"123\") { id title } }",
variables: {},
operationName: "GetComic"
});
```
### 2. Get Schema
```typescript
broker.call("graphql.getSchema");
// Returns: { typeDefs: "...", hasRemoteSchema: true }
```
### 3. Health Check
```typescript
broker.call("graphql.health");
// Returns health status
```
### 4. Refresh Remote Schema
```typescript
broker.call("graphql.refreshRemoteSchema");
// Forces cache refresh
```
## Events
### 1. metadata.imported
Triggered when metadata is imported from external sources.
```typescript
broker.emit("metadata.imported", {
comicId: "123",
source: "COMICVINE"
});
```
**Auto-Resolution:**
If enabled in user preferences, automatically resolves canonical metadata.
### 2. comic.imported
Triggered when a new comic is imported.
```typescript
broker.emit("comic.imported", {
comicId: "123"
});
```
**Auto-Resolution:**
If enabled in user preferences, automatically resolves canonical metadata on import.
## Error Handling Examples
### Client Errors (4xx)
```json
{
"errors": [{
"message": "Invalid ID format",
"extensions": {
"code": "VALIDATION_ERROR",
"field": "id"
}
}]
}
```
### Server Errors (5xx)
```json
{
"errors": [{
"message": "Remote GraphQL service unavailable",
"extensions": {
"code": "SERVICE_UNAVAILABLE",
"context": "Remote schema fetch"
}
}]
}
```
### Timeout Errors
```json
{
"errors": [{
"message": "Query execution timeout after 30000ms",
"extensions": {
"code": "TIMEOUT"
}
}]
}
```
## Best Practices
### 1. Query Optimization
- Use field selection to minimize data transfer
- Implement pagination for large result sets
- Avoid deeply nested queries (max depth: 10)
### 2. Error Handling
- Always check for errors in responses
- Handle specific error codes appropriately
- Log errors for debugging
### 3. Caching
- Use appropriate cache TTL for your use case
- Manually refresh cache when needed
- Monitor cache hit rates
### 4. Monitoring
- Enable health checks in production
- Monitor slow query logs
- Set up alerts for service unavailability
## Troubleshooting
### Remote Schema Connection Issues
**Problem:** Cannot connect to remote metadata service
**Solutions:**
1. Check `METADATA_GRAPHQL_URL` environment variable
2. Verify remote service is running
3. Check network connectivity
4. Review firewall rules
**Fallback:** Service continues with local schema only
### Slow Queries
**Problem:** Queries taking too long
**Solutions:**
1. Check slow query logs
2. Optimize resolver implementations
3. Add database indexes
4. Implement field-level caching
5. Increase timeout if necessary
### Memory Issues
**Problem:** High memory usage
**Solutions:**
1. Reduce cache TTL
2. Disable remote schema caching
3. Implement query complexity limits
4. Add pagination to large queries
### Schema Validation Errors
**Problem:** Schema validation fails
**Solutions:**
1. Check typedef syntax
2. Verify resolver implementations
3. Ensure all types are defined
4. Check for circular dependencies
## Migration Guide
### From Old Implementation
The refactored service maintains backward compatibility with the existing API:
1. **No breaking changes** to GraphQL schema
2. **Same action names** (`graphql.graphql`, `graphql.getSchema`)
3. **Same event handlers** (`metadata.imported`, `comic.imported`)
### New Features
1. **Health endpoint:** `broker.call("graphql.health")`
2. **Schema refresh:** `broker.call("graphql.refreshRemoteSchema")`
3. **Enhanced error messages** with error codes
4. **Performance logging** for slow queries
### Configuration Changes
Old configuration (environment variables only):
```bash
METADATA_GRAPHQL_URL=http://localhost:3080/metadata-graphql
```
New configuration (with defaults):
```bash
# All old variables still work
METADATA_GRAPHQL_URL=http://localhost:3080/metadata-graphql
# New optional variables
GRAPHQL_REMOTE_TIMEOUT=10000
GRAPHQL_CACHE_ENABLED=true
GRAPHQL_EXECUTION_TIMEOUT=30000
```
## Testing
### Unit Tests
```typescript
// Test schema initialization
describe("GraphQL Service", () => {
it("should initialize local schema", async () => {
const schema = await service.initializeLocalSchema();
expect(schema).toBeDefined();
});
it("should handle remote schema failure gracefully", async () => {
// Mock remote schema failure
const schema = await service.started();
expect(schema).toBe(localSchema);
});
});
```
### Integration Tests
```typescript
// Test query execution
describe("GraphQL Queries", () => {
it("should execute comic query", async () => {
const result = await broker.call("graphql.graphql", {
query: "query { comic(id: \"123\") { id title } }"
});
expect(result.data).toBeDefined();
});
});
```
## Performance Benchmarks
Typical performance metrics:
- **Local query:** 10-50ms
- **Remote query:** 100-500ms (depending on network)
- **Stitched query:** 150-600ms
- **Cached remote schema:** +0ms overhead
## Security Considerations
1. **Query Depth Limiting:** Prevents deeply nested queries (DoS protection)
2. **Query Length Limiting:** Prevents excessively large queries
3. **Input Sanitization:** Removes control characters and validates formats
4. **Error Sanitization:** Hides sensitive information in production
5. **Timeout Protection:** Prevents long-running queries from blocking
## Future Enhancements
1. **Query Complexity Analysis:** Calculate and limit query complexity
2. **Rate Limiting:** Per-client rate limiting
3. **Persisted Queries:** Pre-approved query whitelist
4. **DataLoader Integration:** Batch and cache database queries
5. **Subscription Support:** Real-time updates via WebSocket
6. **Field-Level Caching:** Cache individual field results
7. **Distributed Tracing:** OpenTelemetry integration
## Support
For issues or questions:
1. Check this documentation
2. Review error logs
3. Check health endpoint
4. Review configuration
5. Open an issue on GitHub

View File

@@ -1,139 +0,0 @@
/**
* 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

@@ -1,248 +0,0 @@
/**
* 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

@@ -1,117 +0,0 @@
/**
* GraphQL queries and mutations for import operations
* @module examples/frontend/graphql-queries/importQueries
*/
/**
* Query to get import statistics for a directory
* Shows how many files are already imported vs. new files
*/
export const GET_IMPORT_STATISTICS = `
query GetImportStatistics($directoryPath: String) {
getImportStatistics(directoryPath: $directoryPath) {
success
directory
stats {
totalLocalFiles
alreadyImported
newFiles
percentageImported
}
}
}
`;
/**
* Mutation to start a new full import
* Imports all comics in the directory, skipping already imported files
*/
export const START_NEW_IMPORT = `
mutation StartNewImport($sessionId: String!) {
startNewImport(sessionId: $sessionId) {
success
message
jobsQueued
}
}
`;
/**
* Mutation to start an incremental import
* Only imports new files not already in the database
*/
export const START_INCREMENTAL_IMPORT = `
mutation StartIncrementalImport($sessionId: String!, $directoryPath: String) {
startIncrementalImport(sessionId: $sessionId, directoryPath: $directoryPath) {
success
message
stats {
total
alreadyImported
newFiles
queued
}
}
}
`;
/**
* Example usage with variables
*/
export const exampleUsage = {
// Get import statistics
getStatistics: {
query: GET_IMPORT_STATISTICS,
variables: {
directoryPath: "/comics", // Optional, defaults to COMICS_DIRECTORY
},
},
// Start new full import
startNewImport: {
query: START_NEW_IMPORT,
variables: {
sessionId: `import-${Date.now()}`,
},
},
// Start incremental import
startIncrementalImport: {
query: START_INCREMENTAL_IMPORT,
variables: {
sessionId: `incremental-${Date.now()}`,
directoryPath: "/comics", // Optional
},
},
};
/**
* TypeScript types for the responses
*/
export interface ImportStatistics {
success: boolean;
directory: string;
stats: {
totalLocalFiles: number;
alreadyImported: number;
newFiles: number;
percentageImported: string;
};
}
export interface ImportJobResult {
success: boolean;
message: string;
jobsQueued: number;
}
export interface IncrementalImportResult {
success: boolean;
message: string;
stats: {
total: number;
alreadyImported: number;
newFiles: number;
queued: number;
};
}

View File

@@ -1,260 +0,0 @@
/**
* 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

@@ -1,165 +0,0 @@
/**
* 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

@@ -1,420 +0,0 @@
/**
* Example: Importing Comics with GraphQL and Canonical Metadata
*
* This example demonstrates how to import comics using the new GraphQL-based
* import system that automatically resolves canonical metadata from multiple sources.
*/
import { ServiceBroker } from "moleculer";
import {
importComicViaGraphQL,
updateSourcedMetadataViaGraphQL,
resolveMetadataViaGraphQL,
analyzeMetadataConflictsViaGraphQL,
getComicViaGraphQL,
} from "../utils/import.graphql.utils";
/**
* Example 1: Basic Comic Import
* Import a comic with ComicInfo.xml metadata
*/
async function example1_basicImport(broker: ServiceBroker) {
console.log("\n=== Example 1: Basic Comic Import ===\n");
const result = await importComicViaGraphQL(broker, {
filePath: "/comics/amazing-spider-man-001.cbz",
fileSize: 12345678,
rawFileDetails: {
name: "Amazing Spider-Man 001",
filePath: "/comics/amazing-spider-man-001.cbz",
fileSize: 12345678,
extension: ".cbz",
mimeType: "application/x-cbz",
pageCount: 24,
},
inferredMetadata: {
issue: {
name: "Amazing Spider-Man",
number: 1,
year: "2023",
},
},
sourcedMetadata: {
comicInfo: {
Title: "Amazing Spider-Man #1",
Series: "Amazing Spider-Man",
Number: "1",
Publisher: "Marvel Comics",
Summary: "Peter Parker's origin story begins...",
Year: "2023",
Month: "1",
},
},
});
console.log("Import Result:", {
success: result.success,
message: result.message,
canonicalMetadataResolved: result.canonicalMetadataResolved,
comicId: result.comic.id,
});
console.log("\nCanonical Metadata:");
console.log(" Title:", result.comic.canonicalMetadata?.title?.value);
console.log(" Source:", result.comic.canonicalMetadata?.title?.provenance?.source);
console.log(" Series:", result.comic.canonicalMetadata?.series?.value);
console.log(" Publisher:", result.comic.canonicalMetadata?.publisher?.value);
return result.comic.id;
}
/**
* Example 2: Import with Multiple Sources
* Import a comic with metadata from ComicInfo.xml, ComicVine, and LOCG
*/
async function example2_multiSourceImport(broker: ServiceBroker) {
console.log("\n=== Example 2: Multi-Source Import ===\n");
const result = await importComicViaGraphQL(broker, {
filePath: "/comics/batman-001.cbz",
rawFileDetails: {
name: "Batman 001",
filePath: "/comics/batman-001.cbz",
fileSize: 15000000,
extension: ".cbz",
pageCount: 32,
},
inferredMetadata: {
issue: {
name: "Batman",
number: 1,
year: "2023",
},
},
sourcedMetadata: {
// From ComicInfo.xml
comicInfo: {
Title: "Batman #1",
Series: "Batman",
Number: "1",
Publisher: "DC Comics",
Summary: "The Dark Knight returns...",
},
// From ComicVine API
comicvine: {
name: "Batman #1: The Court of Owls",
issue_number: "1",
description: "A new era begins for the Dark Knight...",
cover_date: "2023-01-01",
volumeInformation: {
name: "Batman",
publisher: {
name: "DC Comics",
},
},
},
// From League of Comic Geeks
locg: {
name: "Batman #1",
publisher: "DC Comics",
description: "Batman faces a new threat...",
rating: 4.8,
pulls: 15000,
cover: "https://example.com/batman-001-cover.jpg",
},
},
});
console.log("Import Result:", {
success: result.success,
canonicalMetadataResolved: result.canonicalMetadataResolved,
comicId: result.comic.id,
});
console.log("\nCanonical Metadata (resolved from 3 sources):");
console.log(" Title:", result.comic.canonicalMetadata?.title?.value);
console.log(" Source:", result.comic.canonicalMetadata?.title?.provenance?.source);
console.log(" Confidence:", result.comic.canonicalMetadata?.title?.provenance?.confidence);
console.log("\n Description:", result.comic.canonicalMetadata?.description?.value?.substring(0, 50) + "...");
console.log(" Source:", result.comic.canonicalMetadata?.description?.provenance?.source);
return result.comic.id;
}
/**
* Example 3: Update Metadata After Import
* Import a comic, then fetch and add ComicVine metadata
*/
async function example3_updateMetadataAfterImport(broker: ServiceBroker) {
console.log("\n=== Example 3: Update Metadata After Import ===\n");
// Step 1: Import with basic metadata
console.log("Step 1: Initial import with ComicInfo.xml only");
const importResult = await importComicViaGraphQL(broker, {
filePath: "/comics/x-men-001.cbz",
rawFileDetails: {
name: "X-Men 001",
filePath: "/comics/x-men-001.cbz",
fileSize: 10000000,
extension: ".cbz",
},
sourcedMetadata: {
comicInfo: {
Title: "X-Men #1",
Series: "X-Men",
Number: "1",
Publisher: "Marvel Comics",
},
},
});
const comicId = importResult.comic.id;
console.log(" Comic imported:", comicId);
console.log(" Initial title:", importResult.comic.canonicalMetadata?.title?.value);
console.log(" Initial source:", importResult.comic.canonicalMetadata?.title?.provenance?.source);
// Step 2: Fetch and add ComicVine metadata
console.log("\nStep 2: Adding ComicVine metadata");
const comicVineData = {
name: "X-Men #1: Mutant Genesis",
issue_number: "1",
description: "The X-Men are reborn in this landmark issue...",
cover_date: "2023-01-01",
volumeInformation: {
name: "X-Men",
publisher: {
name: "Marvel Comics",
},
},
};
const updatedComic = await updateSourcedMetadataViaGraphQL(
broker,
comicId,
"comicvine",
comicVineData
);
console.log(" Updated title:", updatedComic.canonicalMetadata?.title?.value);
console.log(" Updated source:", updatedComic.canonicalMetadata?.title?.provenance?.source);
console.log(" Description added:", updatedComic.canonicalMetadata?.description?.value?.substring(0, 50) + "...");
return comicId;
}
/**
* Example 4: Analyze Metadata Conflicts
* See how conflicts between sources are resolved
*/
async function example4_analyzeConflicts(broker: ServiceBroker) {
console.log("\n=== Example 4: Analyze Metadata Conflicts ===\n");
// Import with conflicting metadata
const result = await importComicViaGraphQL(broker, {
filePath: "/comics/superman-001.cbz",
rawFileDetails: {
name: "Superman 001",
filePath: "/comics/superman-001.cbz",
fileSize: 14000000,
extension: ".cbz",
},
sourcedMetadata: {
comicInfo: {
Title: "Superman #1",
Series: "Superman",
Publisher: "DC Comics",
},
comicvine: {
name: "Superman #1: Man of Steel",
volumeInformation: {
name: "Superman",
publisher: {
name: "DC Comics",
},
},
},
locg: {
name: "Superman #1 (2023)",
publisher: "DC",
},
},
});
const comicId = result.comic.id;
console.log("Comic imported:", comicId);
// Analyze conflicts
console.log("\nAnalyzing metadata conflicts...");
const conflicts = await analyzeMetadataConflictsViaGraphQL(broker, comicId);
console.log(`\nFound ${conflicts.length} field(s) with conflicts:\n`);
for (const conflict of conflicts) {
console.log(`Field: ${conflict.field}`);
console.log(` Candidates:`);
for (const candidate of conflict.candidates) {
console.log(` - "${candidate.value}" from ${candidate.provenance.source} (confidence: ${candidate.provenance.confidence})`);
}
console.log(` Resolved: "${conflict.resolved.value}" from ${conflict.resolved.provenance.source}`);
console.log(` Reason: ${conflict.resolutionReason}`);
console.log();
}
return comicId;
}
/**
* Example 5: Manual Metadata Resolution
* Manually trigger metadata resolution
*/
async function example5_manualResolution(broker: ServiceBroker) {
console.log("\n=== Example 5: Manual Metadata Resolution ===\n");
// Import without auto-resolution (if disabled)
const result = await importComicViaGraphQL(broker, {
filePath: "/comics/wonder-woman-001.cbz",
rawFileDetails: {
name: "Wonder Woman 001",
filePath: "/comics/wonder-woman-001.cbz",
fileSize: 13000000,
extension: ".cbz",
},
sourcedMetadata: {
comicInfo: {
Title: "Wonder Woman #1",
Series: "Wonder Woman",
},
},
});
const comicId = result.comic.id;
console.log("Comic imported:", comicId);
console.log("Auto-resolved:", result.canonicalMetadataResolved);
// Manually trigger resolution
console.log("\nManually resolving metadata...");
const resolvedComic = await resolveMetadataViaGraphQL(broker, comicId);
console.log("Resolved metadata:");
console.log(" Title:", resolvedComic.canonicalMetadata?.title?.value);
console.log(" Series:", resolvedComic.canonicalMetadata?.series?.value);
return comicId;
}
/**
* Example 6: Get Comic with Full Canonical Metadata
* Retrieve a comic with all its canonical metadata
*/
async function example6_getComicWithMetadata(broker: ServiceBroker, comicId: string) {
console.log("\n=== Example 6: Get Comic with Full Metadata ===\n");
const comic = await getComicViaGraphQL(broker, comicId);
console.log("Comic ID:", comic.id);
console.log("\nCanonical Metadata:");
console.log(" Title:", comic.canonicalMetadata?.title?.value);
console.log(" Source:", comic.canonicalMetadata?.title?.provenance?.source);
console.log(" Confidence:", comic.canonicalMetadata?.title?.provenance?.confidence);
console.log(" Fetched:", comic.canonicalMetadata?.title?.provenance?.fetchedAt);
console.log(" User Override:", comic.canonicalMetadata?.title?.userOverride || false);
console.log("\n Series:", comic.canonicalMetadata?.series?.value);
console.log(" Source:", comic.canonicalMetadata?.series?.provenance?.source);
console.log("\n Publisher:", comic.canonicalMetadata?.publisher?.value);
console.log(" Source:", comic.canonicalMetadata?.publisher?.provenance?.source);
if (comic.canonicalMetadata?.description) {
console.log("\n Description:", comic.canonicalMetadata.description.value?.substring(0, 100) + "...");
console.log(" Source:", comic.canonicalMetadata.description.provenance?.source);
}
if (comic.canonicalMetadata?.creators?.length > 0) {
console.log("\n Creators:");
for (const creator of comic.canonicalMetadata.creators) {
console.log(` - ${creator.name} (${creator.role}) from ${creator.provenance.source}`);
}
}
console.log("\nRaw File Details:");
console.log(" Name:", comic.rawFileDetails?.name);
console.log(" Path:", comic.rawFileDetails?.filePath);
console.log(" Size:", comic.rawFileDetails?.fileSize);
console.log(" Pages:", comic.rawFileDetails?.pageCount);
console.log("\nImport Status:");
console.log(" Imported:", comic.importStatus?.isImported);
console.log(" Tagged:", comic.importStatus?.tagged);
}
/**
* Run all examples
*/
async function runAllExamples(broker: ServiceBroker) {
console.log("╔════════════════════════════════════════════════════════════╗");
console.log("║ Comic Import with GraphQL & Canonical Metadata Examples ║");
console.log("╚════════════════════════════════════════════════════════════╝");
try {
// Example 1: Basic import
const comicId1 = await example1_basicImport(broker);
// Example 2: Multi-source import
const comicId2 = await example2_multiSourceImport(broker);
// Example 3: Update after import
const comicId3 = await example3_updateMetadataAfterImport(broker);
// Example 4: Analyze conflicts
const comicId4 = await example4_analyzeConflicts(broker);
// Example 5: Manual resolution
const comicId5 = await example5_manualResolution(broker);
// Example 6: Get full metadata
await example6_getComicWithMetadata(broker, comicId2);
console.log("\n╔════════════════════════════════════════════════════════════╗");
console.log("║ All examples completed successfully! ║");
console.log("╚════════════════════════════════════════════════════════════╝\n");
} catch (error) {
console.error("\n❌ Error running examples:", error);
throw error;
}
}
/**
* Usage in your service
*/
export {
example1_basicImport,
example2_multiSourceImport,
example3_updateMetadataAfterImport,
example4_analyzeConflicts,
example5_manualResolution,
example6_getComicWithMetadata,
runAllExamples,
};
// If running directly
if (require.main === module) {
console.log("Note: This is an example file. To run these examples:");
console.log("1. Ensure your Moleculer broker is running");
console.log("2. Import and call the example functions from your service");
console.log("3. Or integrate the patterns into your library.service.ts");
}

View File

@@ -1,347 +0,0 @@
/**
* Example: Incremental Import
*
* This example demonstrates how to use the incremental import feature
* to import only new files that haven't been previously imported.
*/
import { ServiceBroker } from "moleculer";
import {
getImportedFilePaths,
getImportedFileNames,
getImportStatistics,
batchCheckImported,
getComicsNeedingReimport,
findDuplicateFiles,
} from "../utils/import.utils";
/**
* Example 1: Basic Incremental Import
* Import only new files from your comics directory
*/
async function example1_basicIncrementalImport(broker: ServiceBroker) {
console.log("\n=== Example 1: Basic Incremental Import ===\n");
try {
// Call the incremental import endpoint
const result: any = await broker.call("library.incrementalImport", {
sessionId: "incremental-session-" + Date.now(),
});
console.log("Import Result:");
console.log(` Success: ${result.success}`);
console.log(` Message: ${result.message}`);
console.log("\nStatistics:");
console.log(` Total files found: ${result.stats.total}`);
console.log(` Already imported: ${result.stats.alreadyImported}`);
console.log(` New files: ${result.stats.newFiles}`);
console.log(` Queued for import: ${result.stats.queued}`);
return result;
} catch (error) {
console.error("Error during incremental import:", error);
throw error;
}
}
/**
* Example 2: Get Import Statistics
* Check how many files are imported vs. new without starting an import
*/
async function example2_getImportStatistics(broker: ServiceBroker) {
console.log("\n=== Example 2: Get Import Statistics ===\n");
try {
const result: any = await broker.call("library.getImportStatistics", {
// Optional: specify a custom directory path
// directoryPath: "/path/to/comics"
});
console.log("Import Statistics:");
console.log(` Directory: ${result.directory}`);
console.log(` Total local files: ${result.stats.totalLocalFiles}`);
console.log(` Already imported: ${result.stats.alreadyImported}`);
console.log(` New files to import: ${result.stats.newFiles}`);
console.log(` Percentage imported: ${result.stats.percentageImported}`);
return result;
} catch (error) {
console.error("Error getting import statistics:", error);
throw error;
}
}
/**
* Example 3: Check Specific Files
* Check if specific files are already imported
*/
async function example3_checkSpecificFiles() {
console.log("\n=== Example 3: Check Specific Files ===\n");
const filesToCheck = [
"/comics/batman-001.cbz",
"/comics/superman-001.cbz",
"/comics/wonder-woman-001.cbz",
];
try {
const results = await batchCheckImported(filesToCheck);
console.log("File Import Status:");
results.forEach((isImported, filePath) => {
console.log(` ${filePath}: ${isImported ? "✓ Imported" : "✗ Not imported"}`);
});
return results;
} catch (error) {
console.error("Error checking files:", error);
throw error;
}
}
/**
* Example 4: Get All Imported File Paths
* Retrieve a list of all imported file paths from the database
*/
async function example4_getAllImportedPaths() {
console.log("\n=== Example 4: Get All Imported File Paths ===\n");
try {
const importedPaths = await getImportedFilePaths();
console.log(`Total imported files: ${importedPaths.size}`);
// Show first 10 as examples
const pathArray = Array.from(importedPaths);
console.log("\nFirst 10 imported files:");
pathArray.slice(0, 10).forEach((path, index) => {
console.log(` ${index + 1}. ${path}`);
});
if (pathArray.length > 10) {
console.log(` ... and ${pathArray.length - 10} more`);
}
return importedPaths;
} catch (error) {
console.error("Error getting imported paths:", error);
throw error;
}
}
/**
* Example 5: Get All Imported File Names
* Retrieve a list of all imported file names (without paths)
*/
async function example5_getAllImportedNames() {
console.log("\n=== Example 5: Get All Imported File Names ===\n");
try {
const importedNames = await getImportedFileNames();
console.log(`Total imported file names: ${importedNames.size}`);
// Show first 10 as examples
const nameArray = Array.from(importedNames);
console.log("\nFirst 10 imported file names:");
nameArray.slice(0, 10).forEach((name, index) => {
console.log(` ${index + 1}. ${name}`);
});
if (nameArray.length > 10) {
console.log(` ... and ${nameArray.length - 10} more`);
}
return importedNames;
} catch (error) {
console.error("Error getting imported names:", error);
throw error;
}
}
/**
* Example 6: Find Comics Needing Re-import
* Find comics that have files but incomplete metadata
*/
async function example6_findComicsNeedingReimport() {
console.log("\n=== Example 6: Find Comics Needing Re-import ===\n");
try {
const comics = await getComicsNeedingReimport();
console.log(`Found ${comics.length} comics needing re-import`);
if (comics.length > 0) {
console.log("\nFirst 5 comics needing re-import:");
comics.slice(0, 5).forEach((comic: any, index) => {
console.log(` ${index + 1}. ${comic.rawFileDetails?.name || "Unknown"}`);
console.log(` Path: ${comic.rawFileDetails?.filePath || "N/A"}`);
console.log(` Has title: ${!!comic.canonicalMetadata?.title?.value}`);
console.log(` Has series: ${!!comic.canonicalMetadata?.series?.value}`);
});
if (comics.length > 5) {
console.log(` ... and ${comics.length - 5} more`);
}
}
return comics;
} catch (error) {
console.error("Error finding comics needing re-import:", error);
throw error;
}
}
/**
* Example 7: Find Duplicate Files
* Find files with the same name but different paths
*/
async function example7_findDuplicates() {
console.log("\n=== Example 7: Find Duplicate Files ===\n");
try {
const duplicates = await findDuplicateFiles();
console.log(`Found ${duplicates.length} duplicate file names`);
if (duplicates.length > 0) {
console.log("\nDuplicate files:");
duplicates.slice(0, 5).forEach((dup, index) => {
console.log(` ${index + 1}. ${dup.name} (${dup.count} copies)`);
dup.paths.forEach((path: string) => {
console.log(` - ${path}`);
});
});
if (duplicates.length > 5) {
console.log(` ... and ${duplicates.length - 5} more`);
}
}
return duplicates;
} catch (error) {
console.error("Error finding duplicates:", error);
throw error;
}
}
/**
* Example 8: Custom Import Statistics for Specific Directory
* Get statistics for a custom directory path
*/
async function example8_customDirectoryStats(directoryPath: string) {
console.log("\n=== Example 8: Custom Directory Statistics ===\n");
console.log(`Analyzing directory: ${directoryPath}`);
try {
const klaw = require("klaw");
const through2 = require("through2");
const path = require("path");
// Collect all comic files in the custom directory
const localFiles: string[] = [];
await new Promise<void>((resolve, reject) => {
klaw(directoryPath)
.on("error", (err: Error) => {
console.error(`Error walking directory:`, err);
reject(err);
})
.pipe(
through2.obj(function (item: any, enc: any, next: any) {
const fileExtension = path.extname(item.path);
if ([".cbz", ".cbr", ".cb7"].includes(fileExtension)) {
localFiles.push(item.path);
}
next();
})
)
.on("end", () => {
resolve();
});
});
// Get statistics
const stats = await getImportStatistics(localFiles);
console.log("\nStatistics:");
console.log(` Total files: ${stats.total}`);
console.log(` Already imported: ${stats.alreadyImported}`);
console.log(` New files: ${stats.newFiles}`);
console.log(` Percentage: ${((stats.alreadyImported / stats.total) * 100).toFixed(2)}%`);
return stats;
} catch (error) {
console.error("Error getting custom directory stats:", error);
throw error;
}
}
/**
* Run all examples
*/
async function runAllExamples(broker: ServiceBroker) {
console.log("╔════════════════════════════════════════════════════════════╗");
console.log("║ Incremental Import Examples ║");
console.log("╚════════════════════════════════════════════════════════════╝");
try {
// Example 1: Basic incremental import
await example1_basicIncrementalImport(broker);
// Example 2: Get statistics without importing
await example2_getImportStatistics(broker);
// Example 3: Check specific files
await example3_checkSpecificFiles();
// Example 4: Get all imported paths
await example4_getAllImportedPaths();
// Example 5: Get all imported names
await example5_getAllImportedNames();
// Example 6: Find comics needing re-import
await example6_findComicsNeedingReimport();
// Example 7: Find duplicates
await example7_findDuplicates();
// Example 8: Custom directory stats (uncomment and provide path)
// await example8_customDirectoryStats("/path/to/custom/comics");
console.log("\n╔════════════════════════════════════════════════════════════╗");
console.log("║ All examples completed successfully! ║");
console.log("╚════════════════════════════════════════════════════════════╝\n");
} catch (error) {
console.error("\n❌ Error running examples:", error);
throw error;
}
}
/**
* Usage in your service or application
*/
export {
example1_basicIncrementalImport,
example2_getImportStatistics,
example3_checkSpecificFiles,
example4_getAllImportedPaths,
example5_getAllImportedNames,
example6_findComicsNeedingReimport,
example7_findDuplicates,
example8_customDirectoryStats,
runAllExamples,
};
// If running directly
if (require.main === module) {
console.log("Note: This is an example file. To run these examples:");
console.log("1. Ensure your Moleculer broker is running");
console.log("2. Import and call the example functions from your service");
console.log("3. Or integrate the patterns into your application");
console.log("\nQuick Start:");
console.log(" - Use example1_basicIncrementalImport() to import only new files");
console.log(" - Use example2_getImportStatistics() to check status before importing");
console.log(" - Use example3_checkSpecificFiles() to verify specific files");
}

View File

@@ -1,87 +0,0 @@
#!/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"