feat: implement comprehensive GraphQL read interface (issue #9)
Adds a complete GraphQL API for querying MarkiTect database content including: CORE FEATURES: - Type-safe GraphQL schema with comprehensive field definitions - Full database access: markdown files, schemas, ASTs, and metadata - Advanced search capabilities with relevance scoring - Pagination support for efficient data access - Real-time schema introspection and development tools IMPLEMENTATION: - GraphQL schema definition with 6 core types (MarkdownFile, Schema, AST, etc.) - Complete resolver implementation with database integration - Flask-based GraphQL server with CORS support - GraphQL Playground for interactive development - Health check and schema introspection endpoints CLI INTEGRATION: - graphql-serve: Start GraphQL server with customizable options - graphql-query: Execute queries from command line (local/remote) - graphql-schema: Retrieve schema definition in SDL/JSON format - graphql-examples: Comprehensive usage examples and documentation API FEATURES: - Single item queries (by ID or filename) - List queries with filtering and pagination - Full-text search across files and schemas - Database statistics and analytics - AST querying with JSONPath expressions - Computed fields (word count, line count, etc.) TESTING: - Comprehensive test suite with 38 passing tests - Unit tests for schema, resolvers, server, and client - Integration tests for query execution - Error handling and edge case coverage - Mock and fixture support for isolated testing DOCUMENTATION: - Complete API documentation with examples - Usage guide for all CLI commands - Programming examples in Python and JavaScript - Performance optimization guidelines - Troubleshooting and security considerations The GraphQL interface enables developers to build rich applications on top of MarkiTect data with flexible, efficient querying capabilities. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
255
markitect/graphql/server.py
Normal file
255
markitect/graphql/server.py
Normal file
@@ -0,0 +1,255 @@
|
||||
"""
|
||||
GraphQL server implementation for MarkiTect.
|
||||
|
||||
Provides a standalone GraphQL server and integration components
|
||||
for serving the MarkiTect GraphQL API.
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Optional, Dict, Any
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from flask import Flask, request, jsonify
|
||||
from flask_cors import CORS
|
||||
FLASK_AVAILABLE = True
|
||||
except ImportError:
|
||||
FLASK_AVAILABLE = False
|
||||
|
||||
from .schema import schema
|
||||
from .resolvers import Query
|
||||
|
||||
|
||||
class GraphQLServer:
|
||||
"""GraphQL server for MarkiTect API."""
|
||||
|
||||
def __init__(self, db_path: Optional[str] = None, enable_cors: bool = True):
|
||||
"""
|
||||
Initialize GraphQL server.
|
||||
|
||||
Args:
|
||||
db_path: Path to MarkiTect database
|
||||
enable_cors: Enable CORS for web browser access
|
||||
"""
|
||||
self.db_path = db_path or self._get_default_db_path()
|
||||
self.enable_cors = enable_cors
|
||||
self.app = None
|
||||
|
||||
if not FLASK_AVAILABLE:
|
||||
raise ImportError(
|
||||
"Flask is required for GraphQL server. Install with: pip install flask flask-cors"
|
||||
)
|
||||
|
||||
def _get_default_db_path(self) -> str:
|
||||
"""Get default database path."""
|
||||
from .resolvers import get_default_database_path
|
||||
return get_default_database_path()
|
||||
|
||||
def create_app(self) -> Flask:
|
||||
"""Create Flask application with GraphQL endpoint."""
|
||||
app = Flask(__name__)
|
||||
|
||||
if self.enable_cors:
|
||||
CORS(app)
|
||||
|
||||
@app.route('/graphql', methods=['POST'])
|
||||
def graphql_endpoint():
|
||||
"""Handle GraphQL requests."""
|
||||
try:
|
||||
# Parse request data
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({'error': 'No JSON data provided'}), 400
|
||||
|
||||
query = data.get('query')
|
||||
variables = data.get('variables', {})
|
||||
operation_name = data.get('operationName')
|
||||
|
||||
if not query:
|
||||
return jsonify({'error': 'No query provided'}), 400
|
||||
|
||||
# Execute GraphQL query
|
||||
result = schema.execute(
|
||||
query,
|
||||
variables=variables,
|
||||
operation_name=operation_name,
|
||||
context={'db_path': self.db_path}
|
||||
)
|
||||
|
||||
# Format response
|
||||
response_data = {'data': result.data}
|
||||
if result.errors:
|
||||
response_data['errors'] = [
|
||||
{'message': str(error)} for error in result.errors
|
||||
]
|
||||
|
||||
return jsonify(response_data)
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'errors': [{'message': f'Server error: {str(e)}'}]
|
||||
}), 500
|
||||
|
||||
@app.route('/graphql', methods=['GET'])
|
||||
def graphql_playground():
|
||||
"""Serve GraphQL playground for development."""
|
||||
return '''
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>MarkiTect GraphQL Playground</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/graphql-playground-react/build/static/css/index.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root">
|
||||
<style>
|
||||
body { margin: 0; font-family: Open Sans, sans-serif; overflow: hidden; }
|
||||
#root { height: 100vh; }
|
||||
</style>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/graphql-playground-react/build/static/js/middleware.js"></script>
|
||||
<script>
|
||||
window.addEventListener('load', function (event) {
|
||||
GraphQLPlayground.init(document.getElementById('root'), {
|
||||
endpoint: '/graphql',
|
||||
settings: {
|
||||
'general.betaUpdates': false,
|
||||
'editor.theme': 'dark',
|
||||
'editor.reuseHeaders': true,
|
||||
'tracing.hideTracingResponse': true,
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
@app.route('/schema', methods=['GET'])
|
||||
def get_schema():
|
||||
"""Get GraphQL schema definition."""
|
||||
try:
|
||||
from graphql.utilities import print_schema
|
||||
schema_sdl = print_schema(schema.graphql_schema)
|
||||
except (AttributeError, ImportError):
|
||||
# Fallback to simple introspection
|
||||
schema_sdl = str(schema)
|
||||
|
||||
return jsonify({
|
||||
'schema': schema_sdl
|
||||
})
|
||||
|
||||
@app.route('/health', methods=['GET'])
|
||||
def health_check():
|
||||
"""Health check endpoint."""
|
||||
try:
|
||||
# Test database connection
|
||||
import sqlite3
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT 1")
|
||||
conn.close()
|
||||
|
||||
return jsonify({
|
||||
'status': 'healthy',
|
||||
'database': 'connected',
|
||||
'database_path': self.db_path
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'unhealthy',
|
||||
'database': 'error',
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
self.app = app
|
||||
return app
|
||||
|
||||
def run(self, host: str = '127.0.0.1', port: int = 5000, debug: bool = False):
|
||||
"""
|
||||
Run the GraphQL server.
|
||||
|
||||
Args:
|
||||
host: Host to bind to
|
||||
port: Port to bind to
|
||||
debug: Enable debug mode
|
||||
"""
|
||||
if not self.app:
|
||||
self.create_app()
|
||||
|
||||
print(f"🚀 MarkiTect GraphQL Server starting...")
|
||||
print(f"🔗 GraphQL endpoint: http://{host}:{port}/graphql")
|
||||
print(f"🎮 GraphQL playground: http://{host}:{port}/graphql")
|
||||
print(f"📊 Schema introspection: http://{host}:{port}/schema")
|
||||
print(f"❤️ Health check: http://{host}:{port}/health")
|
||||
|
||||
self.app.run(host=host, port=port, debug=debug)
|
||||
|
||||
|
||||
class GraphQLClient:
|
||||
"""Simple GraphQL client for testing and CLI integration."""
|
||||
|
||||
def __init__(self, endpoint: str = "http://localhost:5000/graphql"):
|
||||
"""
|
||||
Initialize GraphQL client.
|
||||
|
||||
Args:
|
||||
endpoint: GraphQL endpoint URL
|
||||
"""
|
||||
self.endpoint = endpoint
|
||||
|
||||
def execute(self, query: str, variables: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Execute GraphQL query.
|
||||
|
||||
Args:
|
||||
query: GraphQL query string
|
||||
variables: Query variables
|
||||
|
||||
Returns:
|
||||
Query result dictionary
|
||||
"""
|
||||
try:
|
||||
import requests
|
||||
|
||||
payload = {
|
||||
'query': query,
|
||||
'variables': variables or {}
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
self.endpoint,
|
||||
json=payload,
|
||||
headers={'Content-Type': 'application/json'}
|
||||
)
|
||||
|
||||
return response.json()
|
||||
|
||||
except ImportError:
|
||||
raise ImportError("requests is required for GraphQL client. Install with: pip install requests")
|
||||
|
||||
def execute_local(self, query: str, variables: Optional[Dict[str, Any]] = None, context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Execute GraphQL query directly (without HTTP).
|
||||
|
||||
Args:
|
||||
query: GraphQL query string
|
||||
variables: Query variables
|
||||
context: GraphQL context
|
||||
|
||||
Returns:
|
||||
Query result dictionary
|
||||
"""
|
||||
result = schema.execute(
|
||||
query,
|
||||
variables=variables or {},
|
||||
context=context or {}
|
||||
)
|
||||
|
||||
response_data = {'data': result.data}
|
||||
if result.errors:
|
||||
response_data['errors'] = [
|
||||
{'message': str(error)} for error in result.errors
|
||||
]
|
||||
|
||||
return response_data
|
||||
Reference in New Issue
Block a user