Files
markitect-main/markitect/graphql/schema.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

196 lines
6.8 KiB
Python

"""
GraphQL schema definition for MarkiTect data.
Defines the complete GraphQL schema for querying Markdown files,
ASTs, schemas, and related metadata.
"""
import graphene
from graphene import ObjectType, String, Int, DateTime, List, Field, JSONString
from typing import Optional
class FrontMatter(ObjectType):
"""GraphQL type for front matter data."""
key = String(required=True, description="Front matter key")
value = JSONString(description="Front matter value (can be any JSON type)")
class MarkdownFile(ObjectType):
"""GraphQL type for markdown files stored in MarkiTect."""
id = Int(required=True, description="Unique identifier")
filename = String(required=True, description="File path/name")
content = String(description="Markdown content")
front_matter = List(FrontMatter, description="Parsed front matter data")
front_matter_raw = JSONString(description="Raw front matter as JSON")
created_at = DateTime(description="Creation timestamp")
# Computed fields
word_count = Int(description="Number of words in content")
line_count = Int(description="Number of lines in content")
has_front_matter = graphene.Boolean(description="Whether file has front matter")
def resolve_front_matter(self, info):
"""Resolve front matter as key-value pairs."""
if self.front_matter_raw:
return [
FrontMatter(key=k, value=v)
for k, v in self.front_matter_raw.items()
]
return []
def resolve_word_count(self, info):
"""Calculate word count."""
if self.content:
return len(self.content.split())
return 0
def resolve_line_count(self, info):
"""Calculate line count."""
if self.content:
return len(self.content.splitlines())
return 0
def resolve_has_front_matter(self, info):
"""Check if file has front matter."""
return bool(self.front_matter_raw)
class Schema(ObjectType):
"""GraphQL type for JSON schemas."""
id = Int(required=True, description="Unique identifier")
filename = String(required=True, description="Schema filename")
title = String(description="Schema title")
description = String(description="Schema description")
schema_content = JSONString(required=True, description="JSON schema content")
created_at = DateTime(description="Creation timestamp")
updated_at = DateTime(description="Last update timestamp")
# Computed fields
schema_version = String(description="JSON Schema version")
property_count = Int(description="Number of properties in schema")
def resolve_schema_version(self, info):
"""Extract schema version."""
if self.schema_content and isinstance(self.schema_content, dict):
return self.schema_content.get('$schema', 'Unknown')
return 'Unknown'
def resolve_property_count(self, info):
"""Count properties in schema."""
if (self.schema_content and
isinstance(self.schema_content, dict) and
'properties' in self.schema_content):
return len(self.schema_content['properties'])
return 0
class ASTNode(ObjectType):
"""GraphQL type for AST nodes."""
type = String(required=True, description="Node type")
value = String(description="Node value/content")
level = Int(description="Heading level (for heading nodes)")
children = List(lambda: ASTNode, description="Child nodes")
attrs = JSONString(description="Node attributes")
class AST(ObjectType):
"""GraphQL type for parsed AST."""
file_id = Int(description="Associated file ID")
filename = String(required=True, description="Source filename")
tree = List(ASTNode, description="AST tree structure")
metadata = JSONString(description="AST metadata")
# Statistics
heading_count = Int(description="Number of headings")
link_count = Int(description="Number of links")
image_count = Int(description="Number of images")
code_block_count = Int(description="Number of code blocks")
class DatabaseStats(ObjectType):
"""Database statistics."""
total_files = Int(description="Total number of markdown files")
total_schemas = Int(description="Total number of schemas")
total_size_bytes = Int(description="Total database size in bytes")
last_updated = DateTime(description="Last database update")
class SearchResult(ObjectType):
"""Search result union type."""
type = String(required=True, description="Result type (file, schema)")
score = graphene.Float(description="Search relevance score")
file = Field(MarkdownFile, description="Matched file (if type=file)")
schema = Field(Schema, description="Matched schema (if type=schema)")
highlight = String(description="Highlighted match text")
class Query(ObjectType):
"""Root GraphQL query type."""
# Single item queries
markdown_file = Field(
MarkdownFile,
id=Int(description="File ID"),
filename=String(description="File path"),
description="Get a specific markdown file"
)
schema = Field(
Schema,
id=Int(description="Schema ID"),
filename=String(description="Schema filename"),
description="Get a specific schema"
)
ast = Field(
AST,
file_id=Int(description="File ID"),
filename=String(description="File path"),
description="Get AST for a specific file"
)
# List queries
markdown_files = List(
MarkdownFile,
limit=Int(default_value=50, description="Maximum number of results"),
offset=Int(default_value=0, description="Offset for pagination"),
has_front_matter=graphene.Boolean(description="Filter by front matter presence"),
created_after=DateTime(description="Filter by creation date"),
description="List markdown files with optional filtering"
)
schemas = List(
Schema,
limit=Int(default_value=50, description="Maximum number of results"),
offset=Int(default_value=0, description="Offset for pagination"),
description="List all schemas"
)
# Search
search = List(
SearchResult,
query=String(required=True, description="Search query"),
type=String(description="Search type filter (file, schema, all)"),
limit=Int(default_value=20, description="Maximum number of results"),
description="Search across files and schemas"
)
# Statistics
database_stats = Field(
DatabaseStats,
description="Get database statistics"
)
# JSONPath queries for ASTs
ast_query = List(
JSONString,
file_id=Int(),
filename=String(),
jsonpath=String(required=True, description="JSONPath expression"),
description="Query AST using JSONPath expressions"
)
# Create the schema
schema = graphene.Schema(query=Query)