diff --git a/docs/GRAPHQL_SERVICE.md b/docs/GRAPHQL_SERVICE.md deleted file mode 100644 index de0393a..0000000 --- a/docs/GRAPHQL_SERVICE.md +++ /dev/null @@ -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 diff --git a/examples/frontend/components/ComicDetailContainer.tsx b/examples/frontend/components/ComicDetailContainer.tsx deleted file mode 100644 index 13aaf48..0000000 --- a/examples/frontend/components/ComicDetailContainer.tsx +++ /dev/null @@ -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( - 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 ( -
-
- Error loading comic: - - {error instanceof Error ? error.message : 'Unknown error'} - -
-
- ); - } - - if (isLoading) { - return ( -
-
-
- Loading comic details... -
-
-
- ); - } - - return ( - comicBookDetailData?.data && ( - - ) - ); -}; - -/** - * 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( - 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 -}; -*/ diff --git a/examples/frontend/graphql-queries/comicDetail.ts b/examples/frontend/graphql-queries/comicDetail.ts deleted file mode 100644 index fc36633..0000000 --- a/examples/frontend/graphql-queries/comicDetail.ts +++ /dev/null @@ -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 - } - } - } - } - } -`; diff --git a/examples/frontend/graphql-queries/importQueries.ts b/examples/frontend/graphql-queries/importQueries.ts deleted file mode 100644 index 09865cf..0000000 --- a/examples/frontend/graphql-queries/importQueries.ts +++ /dev/null @@ -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; - }; -} diff --git a/examples/frontend/graphql-queries/libraryQueries.ts b/examples/frontend/graphql-queries/libraryQueries.ts deleted file mode 100644 index 0628661..0000000 --- a/examples/frontend/graphql-queries/libraryQueries.ts +++ /dev/null @@ -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, - }, - }, -}; diff --git a/examples/frontend/services/GraphQLApi.ts b/examples/frontend/services/GraphQLApi.ts deleted file mode 100644 index 561b7a5..0000000 --- a/examples/frontend/services/GraphQLApi.ts +++ /dev/null @@ -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( - * GET_COMIC_DETAIL_QUERY, - * { id: 'comic-id-123' } - * ); - * console.log(result.comic.rawFileDetails.name); - * ``` - */ -export const executeGraphQLQuery = async ( - query: string, - variables?: Record -): Promise => { - 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 ( - mutation: string, - variables?: Record -): Promise => { - // Mutations use the same endpoint as queries - return executeGraphQLQuery(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), - }; -}; diff --git a/examples/import-comic-graphql.example.ts b/examples/import-comic-graphql.example.ts deleted file mode 100644 index 13ffce5..0000000 --- a/examples/import-comic-graphql.example.ts +++ /dev/null @@ -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"); -} diff --git a/examples/incremental-import.example.ts b/examples/incremental-import.example.ts deleted file mode 100644 index d222f54..0000000 --- a/examples/incremental-import.example.ts +++ /dev/null @@ -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((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"); -} diff --git a/examples/test-graphql-endpoint.sh b/examples/test-graphql-endpoint.sh deleted file mode 100755 index 958b753..0000000 --- a/examples/test-graphql-endpoint.sh +++ /dev/null @@ -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"