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:
196
markitect/graphql/schema.py
Normal file
196
markitect/graphql/schema.py
Normal file
@@ -0,0 +1,196 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user