Files
markitect-main/markitect/graphql/server.py
tegwick 2dd1704e51 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>
2025-10-03 11:53:53 +02:00

255 lines
8.0 KiB
Python

"""
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