⛔ removed useless files
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
};
|
||||
*/
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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),
|
||||
};
|
||||
};
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user