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:
2025-10-03 11:53:53 +02:00
parent c4a1b3cc0c
commit 2dd1704e51
7 changed files with 2626 additions and 0 deletions

View File

@@ -5280,6 +5280,404 @@ def plugin_discover(config, refresh):
sys.exit(1)
# GraphQL Commands
@cli.command(name='graphql-serve')
@click.option('--host', default='127.0.0.1', help='Host to bind to')
@click.option('--port', default=5000, type=int, help='Port to bind to')
@click.option('--debug', is_flag=True, help='Enable debug mode')
@click.option('--no-cors', is_flag=True, help='Disable CORS')
@pass_config
def graphql_serve(config, host, port, debug, no_cors):
"""Start GraphQL server for MarkiTect API.
Starts a GraphQL server that exposes MarkiTect's database content
including markdown files, schemas, and ASTs through a GraphQL interface.
Examples:
markitect graphql-serve
markitect graphql-serve --host 0.0.0.0 --port 8000
markitect graphql-serve --debug --no-cors
"""
try:
from .graphql import GraphQLServer
server = GraphQLServer(
db_path=config['database_path'],
enable_cors=not no_cors
)
server.run(host=host, port=port, debug=debug)
except ImportError:
click.echo("❌ GraphQL server requires additional dependencies.", err=True)
click.echo("Install with: pip install flask flask-cors", err=True)
sys.exit(1)
except Exception as e:
click.echo(f"❌ Failed to start GraphQL server: {e}", err=True)
if config.get('verbose'):
import traceback
click.echo(traceback.format_exc(), err=True)
sys.exit(1)
@cli.command(name='graphql-query')
@click.argument('query', type=str)
@click.option('--variables', type=str, help='JSON variables for the query')
@click.option('--endpoint', default='http://localhost:5000/graphql', help='GraphQL endpoint URL')
@click.option('--local', is_flag=True, help='Execute query locally without HTTP')
@click.option('--format', 'output_format', type=click.Choice(['json', 'yaml', 'table']),
default='json', help='Output format')
@pass_config
def graphql_query(config, query, variables, endpoint, local, output_format):
"""Execute GraphQL query against MarkiTect API.
Execute GraphQL queries to retrieve data from MarkiTect's database.
Can run against a local server or execute queries directly.
Examples:
markitect graphql-query "{ markdownFiles { id filename } }"
markitect graphql-query "query GetFile($id: Int!) { markdownFile(id: $id) { filename content } }" --variables '{"id": 1}'
markitect graphql-query "{ databaseStats { totalFiles totalSchemas } }" --local
"""
try:
from .graphql import GraphQLClient
import json as json_module
# Parse variables if provided
parsed_variables = {}
if variables:
try:
parsed_variables = json_module.loads(variables)
except json_module.JSONDecodeError as e:
click.echo(f"❌ Invalid JSON in variables: {e}", err=True)
sys.exit(1)
# Execute query
if local:
client = GraphQLClient()
result = client.execute_local(query, parsed_variables)
else:
client = GraphQLClient(endpoint)
result = client.execute(query, parsed_variables)
# Format output
if result.get('errors'):
click.echo("❌ GraphQL Errors:", err=True)
for error in result['errors']:
click.echo(f" {error.get('message', str(error))}", err=True)
if result.get('data'):
if output_format == 'json':
click.echo(json_module.dumps(result['data'], indent=2))
elif output_format == 'yaml':
import yaml
click.echo(yaml.dump(result['data'], default_flow_style=False))
elif output_format == 'table':
# Simple table format for basic data
click.echo(format_output(result['data'], 'table'))
except ImportError as e:
if 'requests' in str(e):
click.echo("❌ GraphQL client requires requests library.", err=True)
click.echo("Install with: pip install requests", err=True)
else:
click.echo(f"❌ Missing dependency: {e}", err=True)
sys.exit(1)
except Exception as e:
click.echo(f"❌ Failed to execute GraphQL query: {e}", err=True)
if config.get('verbose'):
import traceback
click.echo(traceback.format_exc(), err=True)
sys.exit(1)
@cli.command(name='graphql-schema')
@click.option('--endpoint', default='http://localhost:5000/graphql', help='GraphQL endpoint URL')
@click.option('--local', is_flag=True, help='Get schema locally without HTTP')
@click.option('--format', 'output_format', type=click.Choice(['sdl', 'json']),
default='sdl', help='Schema format (SDL or introspection JSON)')
@pass_config
def graphql_schema(config, endpoint, local, output_format):
"""Get GraphQL schema definition.
Retrieve and display the GraphQL schema for MarkiTect's API.
Useful for understanding available queries and types.
Examples:
markitect graphql-schema
markitect graphql-schema --local
markitect graphql-schema --format json
"""
try:
if local:
from .graphql import schema
from graphql.utils import schema_printer
if output_format == 'sdl':
click.echo(schema_printer.print_schema(schema))
else:
# For JSON, we'd need introspection query
introspection_query = """
query IntrospectionQuery {
__schema {
queryType { name }
mutationType { name }
subscriptionType { name }
types {
...FullType
}
}
}
fragment FullType on __Type {
kind
name
description
fields(includeDeprecated: true) {
name
description
args {
...InputValue
}
type {
...TypeRef
}
}
}
fragment InputValue on __InputValue {
name
description
type { ...TypeRef }
defaultValue
}
fragment TypeRef on __Type {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
"""
from .graphql import GraphQLClient
client = GraphQLClient()
result = client.execute_local(introspection_query)
import json
click.echo(json.dumps(result, indent=2))
else:
# Get from HTTP endpoint
import requests
if output_format == 'sdl':
# Try to get SDL from /schema endpoint
try:
response = requests.get(endpoint.replace('/graphql', '/schema'))
if response.status_code == 200:
schema_data = response.json()
click.echo(schema_data.get('schema', 'Schema not available'))
else:
click.echo(f"❌ Failed to get schema: HTTP {response.status_code}", err=True)
sys.exit(1)
except requests.RequestException as e:
click.echo(f"❌ Failed to connect to GraphQL server: {e}", err=True)
sys.exit(1)
else:
# Get introspection JSON
from .graphql import GraphQLClient
client = GraphQLClient(endpoint)
# Use introspection query (same as above)
result = client.execute(introspection_query)
import json
click.echo(json.dumps(result, indent=2))
except ImportError as e:
click.echo(f"❌ Missing dependency: {e}", err=True)
sys.exit(1)
except Exception as e:
click.echo(f"❌ Failed to get GraphQL schema: {e}", err=True)
if config.get('verbose'):
import traceback
click.echo(traceback.format_exc(), err=True)
sys.exit(1)
@cli.command(name='graphql-examples')
@pass_config
def graphql_examples(config):
"""Show example GraphQL queries for MarkiTect.
Display common GraphQL query examples to help users get started
with querying MarkiTect's data through the GraphQL interface.
"""
examples = """
📊 MarkiTect GraphQL Query Examples
===================================
🔍 Basic Queries:
# Get all markdown files with basic info
{
markdownFiles(limit: 10) {
id
filename
hasFrontMatter
wordCount
createdAt
}
}
# Get specific file by ID
{
markdownFile(id: 1) {
filename
content
frontMatter {
key
value
}
wordCount
lineCount
}
}
# Get file by filename
{
markdownFile(filename: "docs/README.md") {
id
content
frontMatterRaw
}
}
📋 Schema Queries:
# List all schemas
{
schemas {
id
filename
title
description
schemaVersion
propertyCount
}
}
# Get specific schema
{
schema(id: 1) {
filename
title
schemaContent
createdAt
}
}
🌳 AST Queries:
# Get AST for a file
{
ast(fileId: 1) {
filename
headingCount
linkCount
imageCount
codeBlockCount
tree {
type
value
level
}
}
}
# JSONPath query on AST
{
astQuery(fileId: 1, jsonpath: "$..heading") {
# Returns array of heading nodes
}
}
🔍 Search Queries:
# Search across files and schemas
{
search(query: "markdown", limit: 5) {
type
score
highlight
file {
filename
wordCount
}
schema {
title
description
}
}
}
# Search only files
{
search(query: "README", type: "file") {
type
score
file {
filename
content
}
}
}
📊 Statistics:
# Database overview
{
databaseStats {
totalFiles
totalSchemas
totalSizeBytes
lastUpdated
}
}
🔍 Filtering:
# Files with front matter created after date
{
markdownFiles(
hasFrontMatter: true,
createdAfter: "2023-01-01T00:00:00"
) {
filename
frontMatterRaw
createdAt
}
}
💡 Usage Examples:
# Execute a query locally
markitect graphql-query "{ databaseStats { totalFiles } }" --local
# Execute with variables
markitect graphql-query \\
"query GetFile($id: Int!) { markdownFile(id: $id) { filename } }" \\
--variables '{"id": 1}' --local
# Start GraphQL server
markitect graphql-serve --port 5000
# Query running server
markitect graphql-query "{ markdownFiles { filename } }" \\
--endpoint http://localhost:5000/graphql
"""
click.echo(examples)
# Register issue management commands
cli.add_command(issues_group)