feat: implement GraphQL write interface with mutations (issue #10)
Some checks failed
Test Suite / performance-tests (push) Has been cancelled
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Some checks failed
Test Suite / performance-tests (push) Has been cancelled
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
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>
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
# GraphQL Read Interface for MarkiTect
|
||||
# GraphQL Interface for MarkiTect
|
||||
|
||||
## Overview
|
||||
|
||||
The GraphQL read interface provides a powerful, type-safe way to query 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.
|
||||
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
|
||||
|
||||
@@ -13,6 +13,8 @@ The GraphQL read interface provides a powerful, type-safe way to query MarkiTect
|
||||
- **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
|
||||
|
||||
@@ -265,6 +267,350 @@ Query AST using JSONPath expressions:
|
||||
}
|
||||
```
|
||||
|
||||
## 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:
|
||||
|
||||
```graphql
|
||||
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:
|
||||
|
||||
```graphql
|
||||
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:
|
||||
|
||||
```graphql
|
||||
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:
|
||||
|
||||
```graphql
|
||||
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:
|
||||
|
||||
```graphql
|
||||
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:
|
||||
|
||||
```graphql
|
||||
mutation {
|
||||
deleteSchema(filename: "old-schema.json") {
|
||||
success
|
||||
deletedFilename
|
||||
errors
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Mutation Payload Types
|
||||
|
||||
All mutations return structured payload types with the following pattern:
|
||||
|
||||
#### AddMarkdownFilePayload
|
||||
```graphql
|
||||
type AddMarkdownFilePayload {
|
||||
success: Boolean!
|
||||
markdownFile: MarkdownFile
|
||||
errors: [String!]!
|
||||
}
|
||||
```
|
||||
|
||||
#### UpdateMarkdownFilePayload
|
||||
```graphql
|
||||
type UpdateMarkdownFilePayload {
|
||||
success: Boolean!
|
||||
markdownFile: MarkdownFile
|
||||
errors: [String!]!
|
||||
}
|
||||
```
|
||||
|
||||
#### AddSchemaPayload
|
||||
```graphql
|
||||
type AddSchemaPayload {
|
||||
success: Boolean!
|
||||
schema: Schema
|
||||
errors: [String!]!
|
||||
}
|
||||
```
|
||||
|
||||
#### UpdateSchemaPayload
|
||||
```graphql
|
||||
type UpdateSchemaPayload {
|
||||
success: Boolean!
|
||||
schema: Schema
|
||||
errors: [String!]!
|
||||
}
|
||||
```
|
||||
|
||||
#### DeleteSchemaPayload
|
||||
```graphql
|
||||
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:
|
||||
|
||||
```graphql
|
||||
mutation {
|
||||
addMarkdownFile(filename: "", content: "test") {
|
||||
success # false
|
||||
markdownFile # null
|
||||
errors # ["Filename cannot be empty"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Duplicate Filename Handling
|
||||
Attempting to create files with existing names:
|
||||
|
||||
```graphql
|
||||
mutation {
|
||||
addMarkdownFile(filename: "existing.md", content: "test") {
|
||||
success # false
|
||||
errors # ["File with filename 'existing.md' already exists"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Not Found Errors
|
||||
Updating non-existent resources:
|
||||
|
||||
```graphql
|
||||
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:
|
||||
|
||||
```graphql
|
||||
# 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
|
||||
@@ -305,6 +651,36 @@ markitect graphql-query "query($limit: Int) { markdownFiles(limit: $limit) { fil
|
||||
markitect graphql-query "{ schemas { title } }" --endpoint http://localhost:5000/graphql
|
||||
```
|
||||
|
||||
#### graphql-mutate
|
||||
Execute GraphQL mutations from the command line:
|
||||
```bash
|
||||
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:
|
||||
```bash
|
||||
# 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:
|
||||
```bash
|
||||
@@ -363,6 +739,103 @@ result = client.execute_local("""
|
||||
print(result['data'])
|
||||
```
|
||||
|
||||
### Executing Mutations
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
```python
|
||||
@@ -490,8 +963,14 @@ Use specific search types when possible:
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Read-Only Interface
|
||||
The GraphQL interface is read-only and does not support mutations, providing safe access to MarkiTect data.
|
||||
### 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:
|
||||
|
||||
Reference in New Issue
Block a user