Added comprehensive GraphQL mutations for CRUD operations on markdown files and schemas. Key features: - Complete mutation schema with structured payload types - Markdown file mutations: add, update with front matter support - Schema mutations: add, update, delete with JSON validation - CLI integration with `graphql-mutate` command - Comprehensive error handling and validation - Full test coverage with 24 test cases - Updated documentation with mutation examples and API usage Resolves issue #10: Expose a GraphQL Write Interface 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
22 KiB
GraphQL Interface for MarkiTect
Overview
The GraphQL interface provides a powerful, type-safe way to query and modify MarkiTect's database content including Markdown files, ASTs, schemas, and metadata. This interface enables developers to build rich applications on top of MarkiTect's data with flexible querying capabilities and complete CRUD operations.
Features
- Complete Type Safety: Strongly-typed GraphQL schema with comprehensive field definitions
- Flexible Querying: Query exactly the data you need with GraphQL's selective field syntax
- Rich Data Access: Access markdown files, schemas, ASTs, and computed fields
- Search Capabilities: Full-text search across files and schemas with relevance scoring
- Pagination Support: Efficient pagination for large datasets
- Real-time Introspection: Schema introspection and GraphQL Playground for development
- Multiple Access Methods: HTTP server, local execution, and CLI integration
- Full CRUD Operations: Create, read, update, and delete markdown files and schemas via mutations
- Structured Error Handling: Comprehensive error responses with validation and success indicators
Getting Started
Prerequisites
Install the required dependencies:
pip install graphene flask flask-cors jsonpath-ng requests
Starting the GraphQL Server
Start the GraphQL server using the CLI:
markitect graphql-serve
By default, this starts the server on http://localhost:5000. You can customize the host and port:
markitect graphql-serve --host 0.0.0.0 --port 8080
Accessing the GraphQL Playground
Once the server is running, visit http://localhost:5000/graphql in your browser to access the GraphQL Playground, an interactive development environment for testing queries.
GraphQL Schema
Core Types
MarkdownFile
Represents a Markdown file in the MarkiTect database:
type MarkdownFile {
id: Int!
filename: String!
content: String
frontMatter: [FrontMatter]
frontMatterRaw: JSONString
createdAt: DateTime
wordCount: Int
lineCount: Int
hasFrontMatter: Boolean
}
Schema
Represents a JSON schema:
type Schema {
id: Int!
filename: String!
title: String
description: String
schemaContent: JSONString!
createdAt: DateTime
updatedAt: DateTime
schemaVersion: String
propertyCount: Int
}
AST
Represents an Abstract Syntax Tree for a Markdown file:
type AST {
fileId: Int
filename: String!
tree: [ASTNode]
metadata: JSONString
headingCount: Int
linkCount: Int
imageCount: Int
codeBlockCount: Int
}
DatabaseStats
Provides statistics about the MarkiTect database:
type DatabaseStats {
totalFiles: Int
totalSchemas: Int
totalSizeBytes: Int
lastUpdated: DateTime
}
SearchResult
Represents search results across files and schemas:
type SearchResult {
type: String!
score: Float
file: MarkdownFile
schema: Schema
highlight: String
}
Query Operations
Single Item Queries
Get a specific markdown file:
{
markdownFile(id: 1) {
id
filename
content
wordCount
hasFrontMatter
}
}
Get a specific schema:
{
schema(filename: "user-schema.json") {
title
description
schemaVersion
propertyCount
}
}
Get AST for a file:
{
ast(filename: "document.md") {
headingCount
linkCount
tree {
type
value
level
}
}
}
List Queries
Get all markdown files with pagination:
{
markdownFiles(limit: 10, offset: 0) {
id
filename
createdAt
hasFrontMatter
}
}
Filter files by front matter presence:
{
markdownFiles(hasFrontMatter: true, limit: 5) {
filename
frontMatter {
key
value
}
}
}
Get all schemas:
{
schemas(limit: 20) {
filename
title
description
propertyCount
}
}
Search Queries
Search across all content:
{
search(query: "documentation", type: "all", limit: 10) {
type
score
file {
filename
content
}
schema {
title
description
}
highlight
}
}
Search only in files:
{
search(query: "API", type: "file", limit: 5) {
score
file {
filename
wordCount
}
highlight
}
}
Database Statistics
Get database overview:
{
databaseStats {
totalFiles
totalSchemas
totalSizeBytes
lastUpdated
}
}
Advanced AST Queries
Query AST using JSONPath expressions:
{
astQuery(
filename: "document.md",
jsonpath: "$.children[?(@.type=='heading')]"
)
}
Mutation Operations
The GraphQL interface supports full CRUD operations for markdown files and schemas through mutations. All mutations return structured payloads with success indicators and error handling.
Mutation Types
Markdown File Mutations
Add Markdown File
Create a new markdown file in the database:
mutation {
addMarkdownFile(
filename: "new-document.md"
content: "# New Document\n\nThis is a new markdown file."
) {
success
markdownFile {
id
filename
content
wordCount
hasFrontMatter
createdAt
}
errors
}
}
Add Markdown File with Front Matter
Create a file with YAML front matter:
mutation {
addMarkdownFile(
filename: "blog-post.md"
content: """---
title: "My Blog Post"
date: "2024-01-15"
tags: ["tech", "api"]
---
# My Blog Post
Content goes here.
"""
) {
success
markdownFile {
id
filename
frontMatter {
key
value
}
hasFrontMatter
}
errors
}
}
Update Markdown File
Update an existing markdown file:
mutation {
updateMarkdownFile(
id: 1
content: "# Updated Document\n\nThis content has been updated."
) {
success
markdownFile {
id
filename
content
wordCount
lineCount
}
errors
}
}
Schema Mutations
Add JSON Schema
Create a new JSON schema:
mutation {
addSchema(
filename: "user-schema.json"
schemaContent: {
"type": "object",
"title": "User Schema",
"description": "Schema for user objects",
"properties": {
"name": {"type": "string"},
"email": {"type": "string", "format": "email"},
"age": {"type": "integer", "minimum": 0}
},
"required": ["name", "email"]
}
) {
success
schema {
id
filename
title
description
propertyCount
schemaVersion
}
errors
}
}
Update JSON Schema
Update an existing schema:
mutation {
updateSchema(
id: 1
schemaContent: {
"type": "object",
"title": "Updated User Schema",
"description": "Updated schema for user objects",
"properties": {
"name": {"type": "string"},
"email": {"type": "string", "format": "email"},
"age": {"type": "integer", "minimum": 0},
"country": {"type": "string"}
},
"required": ["name", "email"]
}
) {
success
schema {
id
filename
title
propertyCount
updatedAt
}
errors
}
}
Delete Schema
Remove a schema from the database:
mutation {
deleteSchema(filename: "old-schema.json") {
success
deletedFilename
errors
}
}
Mutation Payload Types
All mutations return structured payload types with the following pattern:
AddMarkdownFilePayload
type AddMarkdownFilePayload {
success: Boolean!
markdownFile: MarkdownFile
errors: [String!]!
}
UpdateMarkdownFilePayload
type UpdateMarkdownFilePayload {
success: Boolean!
markdownFile: MarkdownFile
errors: [String!]!
}
AddSchemaPayload
type AddSchemaPayload {
success: Boolean!
schema: Schema
errors: [String!]!
}
UpdateSchemaPayload
type UpdateSchemaPayload {
success: Boolean!
schema: Schema
errors: [String!]!
}
DeleteSchemaPayload
type DeleteSchemaPayload {
success: Boolean!
deletedFilename: String
errors: [String!]!
}
Error Handling in Mutations
Mutations provide comprehensive error handling:
Validation Errors
Invalid input data returns structured errors:
mutation {
addMarkdownFile(filename: "", content: "test") {
success # false
markdownFile # null
errors # ["Filename cannot be empty"]
}
}
Duplicate Filename Handling
Attempting to create files with existing names:
mutation {
addMarkdownFile(filename: "existing.md", content: "test") {
success # false
errors # ["File with filename 'existing.md' already exists"]
}
}
Not Found Errors
Updating non-existent resources:
mutation {
updateMarkdownFile(id: 999, content: "test") {
success # false
errors # ["Markdown file with ID 999 not found"]
}
}
Complete CRUD Workflow Example
Here's a complete example showing the full lifecycle of a markdown file:
# 1. Create a new file
mutation CreateFile {
addMarkdownFile(
filename: "api-guide.md"
content: """---
title: "API Usage Guide"
version: "1.0"
---
# API Usage Guide
Basic API documentation.
"""
) {
success
markdownFile {
id
filename
hasFrontMatter
}
errors
}
}
# 2. Query the created file
{
markdownFile(filename: "api-guide.md") {
id
content
wordCount
frontMatter {
key
value
}
}
}
# 3. Update the file content
mutation UpdateFile {
updateMarkdownFile(
id: 1
content: """---
title: "API Usage Guide"
version: "2.0"
updated: "2024-01-15"
---
# API Usage Guide
Comprehensive API documentation with examples.
## Authentication
...
"""
) {
success
markdownFile {
id
wordCount
lineCount
frontMatter {
key
value
}
}
errors
}
}
# 4. Verify the update
{
markdownFile(id: 1) {
filename
wordCount
frontMatter {
key
value
}
}
}
CLI Integration
Available Commands
graphql-serve
Start the GraphQL server:
markitect graphql-serve [OPTIONS]
Options:
--host TEXT Host to bind to (default: 127.0.0.1)
--port INTEGER Port to bind to (default: 5000)
--debug Enable debug mode
--no-cors Disable CORS
graphql-query
Execute GraphQL queries from the command line:
markitect graphql-query QUERY [OPTIONS]
Options:
--variables TEXT JSON string of query variables
--endpoint TEXT GraphQL endpoint URL
--local Execute query locally without HTTP
--format [json|yaml|table] Output format (default: json)
Examples:
# Execute a simple query locally
markitect graphql-query "{ databaseStats { totalFiles } }" --local
# Execute query with variables
markitect graphql-query "query($limit: Int) { markdownFiles(limit: $limit) { filename } }" --variables '{"limit": 5}' --local
# Execute query against remote server
markitect graphql-query "{ schemas { title } }" --endpoint http://localhost:5000/graphql
graphql-mutate
Execute GraphQL mutations from the command line:
markitect graphql-mutate MUTATION [OPTIONS]
Options:
--variables TEXT JSON string of mutation variables
--endpoint TEXT GraphQL endpoint URL
--local Execute mutation locally without HTTP
--format [json|yaml|table] Output format (default: json)
Examples:
# Add a new markdown file locally
markitect graphql-mutate 'mutation { addMarkdownFile(filename: "test.md", content: "# Test") { success markdownFile { id filename } errors } }' --local
# Add markdown file with variables
markitect graphql-mutate 'mutation($filename: String!, $content: String!) { addMarkdownFile(filename: $filename, content: $content) { success errors } }' --variables '{"filename": "doc.md", "content": "# Documentation"}' --local
# Update a file remotely
markitect graphql-mutate 'mutation { updateMarkdownFile(id: 1, content: "# Updated") { success errors } }' --endpoint http://localhost:5000/graphql
# Add a JSON schema
markitect graphql-mutate 'mutation { addSchema(filename: "user.json", schemaContent: {"type": "object", "properties": {"name": {"type": "string"}}}) { success schema { id title } errors } }' --local
# Delete a schema
markitect graphql-mutate 'mutation { deleteSchema(filename: "old-schema.json") { success deletedFilename errors } }' --local
graphql-schema
Get the GraphQL schema definition:
markitect graphql-schema [OPTIONS]
Options:
--format [sdl|json] Schema format (default: sdl)
--endpoint TEXT GraphQL endpoint URL
graphql-examples
Show example queries and usage:
markitect graphql-examples
Programming API
Using the GraphQL Client
from markitect.graphql import GraphQLClient
# Create a client
client = GraphQLClient("http://localhost:5000/graphql")
# Execute a query
result = client.execute("""
{
markdownFiles(limit: 5) {
filename
wordCount
}
}
""")
print(result['data'])
Local Execution
from markitect.graphql import GraphQLClient
# Execute queries locally without HTTP
client = GraphQLClient()
result = client.execute_local("""
{
databaseStats {
totalFiles
totalSchemas
}
}
""")
print(result['data'])
Executing Mutations
from markitect.graphql import GraphQLClient
# Create a client
client = GraphQLClient("http://localhost:5000/graphql")
# Add a new markdown file
create_mutation = """
mutation {
addMarkdownFile(
filename: "api-docs.md"
content: "# API Documentation\\n\\nComprehensive API guide."
) {
success
markdownFile {
id
filename
wordCount
}
errors
}
}
"""
result = client.execute(create_mutation)
if result['data']['addMarkdownFile']['success']:
file_id = result['data']['addMarkdownFile']['markdownFile']['id']
print(f"Created file with ID: {file_id}")
else:
print("Errors:", result['data']['addMarkdownFile']['errors'])
# Update the file
update_mutation = """
mutation UpdateFile($id: Int!, $content: String!) {
updateMarkdownFile(id: $id, content: $content) {
success
markdownFile {
wordCount
lineCount
}
errors
}
}
"""
variables = {
"id": file_id,
"content": "# API Documentation\\n\\nUpdated comprehensive API guide with examples."
}
result = client.execute(update_mutation, variables=variables)
print("Update result:", result['data']['updateMarkdownFile']['success'])
Local Mutation Execution
from markitect.graphql import GraphQLClient
# Execute mutations locally without HTTP
client = GraphQLClient()
# Add a JSON schema locally
schema_mutation = """
mutation {
addSchema(
filename: "product-schema.json"
schemaContent: {
"type": "object",
"title": "Product Schema",
"properties": {
"name": {"type": "string"},
"price": {"type": "number", "minimum": 0},
"category": {"type": "string"}
},
"required": ["name", "price"]
}
) {
success
schema {
id
title
propertyCount
}
errors
}
}
"""
result = client.execute_local(schema_mutation)
if result['data']['addSchema']['success']:
schema_info = result['data']['addSchema']['schema']
print(f"Created schema: {schema_info['title']} with {schema_info['propertyCount']} properties")
Using the Server Directly
from markitect.graphql import GraphQLServer
# Create and start server
server = GraphQLServer(db_path="/path/to/markitect.db")
app = server.create_app()
# Start server
server.run(host="0.0.0.0", port=8080, debug=True)
API Endpoints
When running the GraphQL server, the following endpoints are available:
POST /graphql- Main GraphQL endpoint for queriesGET /graphql- GraphQL Playground interfaceGET /schema- Schema introspectionGET /health- Health check endpoint
Health Check Response
{
"status": "healthy",
"database": "connected",
"database_path": "/path/to/markitect.db"
}
Error Handling
The GraphQL interface provides comprehensive error handling:
Query Errors
Invalid GraphQL syntax or non-existent fields return structured errors:
{
"data": null,
"errors": [
{
"message": "Cannot query field 'nonexistentField' on type 'Query'"
}
]
}
Database Errors
Database connectivity issues are handled gracefully:
{
"data": {
"databaseStats": null
},
"errors": [
{
"message": "Database connection error"
}
]
}
Performance Considerations
Pagination
Always use pagination for large datasets:
{
markdownFiles(limit: 50, offset: 0) {
id
filename
}
}
Field Selection
Only query the fields you need:
# Good - selective fields
{
markdownFiles {
id
filename
}
}
# Avoid - requesting large content fields unnecessarily
{
markdownFiles {
id
filename
content # Only include if needed
}
}
Search Optimization
Use specific search types when possible:
# Better - search only files
{
search(query: "API", type: "file", limit: 10) {
file {
filename
}
}
}
# Less efficient - search all types
{
search(query: "API", type: "all", limit: 10) {
type
file { filename }
schema { title }
}
}
Security Considerations
Write Operations Security
The GraphQL interface supports both read and write operations through mutations. Write operations require careful consideration:
- Data Validation: All mutations include comprehensive input validation and structured error responses
- Transaction Safety: Database operations are wrapped in transactions to ensure data consistency
- File Overwrites: Duplicate filenames in the same mutation will overwrite existing files
- Schema Validation: JSON schemas are validated before storage to ensure they are well-formed
- Error Handling: Failed operations return detailed error messages without exposing internal system details
CORS Configuration
CORS is enabled by default for browser access. Disable with --no-cors if needed:
markitect graphql-serve --no-cors
Database Access
The interface uses the same database path configuration as the main MarkiTect CLI, respecting the MARKITECT_DB environment variable.
Troubleshooting
Common Issues
"Flask is required" Error
Install Flask dependencies:
pip install flask flask-cors
"requests is required" Error
Install requests for HTTP client functionality:
pip install requests
Database Connection Errors
Check that your MarkiTect database exists and is accessible:
# Check database location
echo $MARKITECT_DB
# Verify database exists
ls -la ~/.markitect/markitect.db
GraphQL Syntax Errors
Use the GraphQL Playground at http://localhost:5000/graphql to validate your queries with real-time syntax checking and auto-completion.
Examples
Complete Data Export
Export all markdown files with metadata:
{
markdownFiles {
id
filename
content
createdAt
wordCount
lineCount
hasFrontMatter
frontMatter {
key
value
}
}
}
Schema Analysis
Analyze all schemas in the database:
{
schemas {
filename
title
description
schemaVersion
propertyCount
schemaContent
}
}
Content Discovery
Find all documents containing specific terms:
{
search(query: "authentication security", type: "all", limit: 20) {
type
score
file {
filename
wordCount
hasFrontMatter
}
schema {
title
description
}
highlight
}
}
Database Overview
Get a complete overview of your MarkiTect database:
{
databaseStats {
totalFiles
totalSchemas
totalSizeBytes
lastUpdated
}
recentFiles: markdownFiles(limit: 5) {
filename
createdAt
}
recentSchemas: schemas(limit: 5) {
filename
title
createdAt
}
}
Integration Examples
With JavaScript/TypeScript
interface DatabaseStats {
totalFiles: number;
totalSchemas: number;
totalSizeBytes: number;
lastUpdated: string;
}
async function getStats(): Promise<DatabaseStats> {
const response = await fetch('http://localhost:5000/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: `
{
databaseStats {
totalFiles
totalSchemas
totalSizeBytes
lastUpdated
}
}
`
})
});
const { data } = await response.json();
return data.databaseStats;
}
With Python
import requests
from typing import List, Dict, Any
def search_content(query: str, limit: int = 10) -> List[Dict[str, Any]]:
"""Search for content in MarkiTect database."""
response = requests.post('http://localhost:5000/graphql', json={
'query': '''
query SearchContent($query: String!, $limit: Int!) {
search(query: $query, type: "all", limit: $limit) {
type
score
file {
filename
wordCount
}
schema {
title
description
}
highlight
}
}
''',
'variables': {
'query': query,
'limit': limit
}
})
return response.json()['data']['search']
# Usage
results = search_content("API documentation", limit=5)
for result in results:
print(f"Found {result['type']}: {result['highlight']}")
This GraphQL interface provides a powerful, flexible way to access and query MarkiTect data, enabling rich integrations and applications built on top of your Markdown content management system.