Files
markitect-main/markitect/graphql/space_schema.py
tegwick 7de57a389d feat(spaces): implement Phase 6 API Layer
Implements API layer for Information Spaces:
- GraphQL schema types for spaces, documents, variables
- GraphQL queries and mutations for space operations
- CLI command group with all space management commands
- Resolver functions connecting GraphQL to SpaceService
- 38 unit tests for API components

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 12:29:11 +01:00

398 lines
14 KiB
Python

"""
GraphQL schema extension for Information Spaces.
Defines GraphQL types, queries, and mutations for the space system.
"""
import graphene
from graphene import ObjectType, String, Int, DateTime, List, Field, JSONString, Enum
from typing import Optional, Dict, Any
from datetime import datetime
# Enums
class SpaceStatusEnum(Enum):
"""GraphQL enum for space status."""
DRAFT = "draft"
ACTIVE = "active"
ARCHIVED = "archived"
DELETED = "deleted"
class SyncDirectionEnum(Enum):
"""GraphQL enum for sync direction."""
SPACE_TO_DIRECTORY = "space_to_directory"
DIRECTORY_TO_SPACE = "directory_to_space"
BIDIRECTIONAL = "bidirectional"
class ConflictResolutionEnum(Enum):
"""GraphQL enum for conflict resolution strategy."""
SPACE_WINS = "space_wins"
DIRECTORY_WINS = "directory_wins"
NEWER_WINS = "newer_wins"
SKIP = "skip"
# Types
class SpaceConfigType(ObjectType):
"""GraphQL type for space configuration."""
default_variant = String(description="Default export variant")
enable_caching = graphene.Boolean(description="Whether caching is enabled")
theme = String(description="Theme for rendering")
history_enabled = graphene.Boolean(description="Whether history tracking is enabled")
variable_scope = String(description="Default variable scope")
class SpaceMetadataType(ObjectType):
"""GraphQL type for space metadata."""
tags = List(String, description="Tags for categorization")
author = String(description="Author identifier")
custom = JSONString(description="Custom metadata fields")
class InformationSpaceType(ObjectType):
"""GraphQL type for Information Spaces."""
id = String(required=True, description="Unique space identifier")
name = String(required=True, description="Space name")
description = String(description="Space description")
status = String(description="Space lifecycle status")
config = Field(SpaceConfigType, description="Space configuration")
metadata = Field(SpaceMetadataType, description="Space metadata")
parent_space_id = String(description="Parent space ID for inheritance")
created_at = DateTime(description="Creation timestamp")
updated_at = DateTime(description="Last update timestamp")
# Computed fields
document_count = Int(description="Number of documents in space")
variable_count = Int(description="Number of variables defined")
class SpaceDocumentType(ObjectType):
"""GraphQL type for documents in a space."""
id = String(required=True, description="Document association ID")
space_id = String(required=True, description="Parent space ID")
document_id = String(required=True, description="Document identifier")
space_path = String(required=True, description="Path within space")
order_index = Int(description="Order index for sorting")
metadata = JSONString(description="Document-specific metadata")
added_at = DateTime(description="When document was added to space")
class SpaceVariableType(ObjectType):
"""GraphQL type for space variables."""
space_id = String(required=True, description="Parent space ID")
name = String(required=True, description="Variable name")
value = JSONString(description="Variable value")
scope = String(description="Variable scope (space, document, request)")
class RenderResultType(ObjectType):
"""GraphQL type for render results."""
content = String(required=True, description="Rendered content")
format = String(required=True, description="Output format")
content_hash = String(description="Hash of rendered content")
source_hash = String(description="Hash of source content")
document_id = String(description="Rendered document ID")
cached = graphene.Boolean(description="Whether result was from cache")
class SyncResultType(ObjectType):
"""GraphQL type for sync results."""
success = graphene.Boolean(required=True, description="Whether sync succeeded")
created_count = Int(description="Number of items created")
updated_count = Int(description="Number of items updated")
deleted_count = Int(description="Number of items deleted")
conflict_count = Int(description="Number of conflicts encountered")
errors = List(String, description="Error messages")
class SyncConflictType(ObjectType):
"""GraphQL type for sync conflicts."""
path = String(required=True, description="Conflicting path")
space_hash = String(description="Content hash in space")
directory_hash = String(description="Content hash in directory")
resolution = String(description="How conflict was resolved")
winner = String(description="Which side won")
# Input Types
class SpaceConfigInput(graphene.InputObjectType):
"""Input type for space configuration."""
default_variant = String()
enable_caching = graphene.Boolean()
theme = String()
history_enabled = graphene.Boolean()
variable_scope = String()
class SpaceMetadataInput(graphene.InputObjectType):
"""Input type for space metadata."""
tags = List(String)
author = String()
custom = JSONString()
class CreateSpaceInput(graphene.InputObjectType):
"""Input for creating a space."""
name = String(required=True)
description = String()
config = graphene.Argument(SpaceConfigInput)
metadata = graphene.Argument(SpaceMetadataInput)
parent_space_id = String()
class UpdateSpaceInput(graphene.InputObjectType):
"""Input for updating a space."""
name = String()
description = String()
config = graphene.Argument(SpaceConfigInput)
metadata = graphene.Argument(SpaceMetadataInput)
class AddDocumentInput(graphene.InputObjectType):
"""Input for adding a document to a space."""
document_id = String(required=True)
space_path = String(required=True)
order_index = Int()
metadata = JSONString()
class RenderOptionsInput(graphene.InputObjectType):
"""Input for render options."""
theme = String()
include_toc = graphene.Boolean()
force_refresh = graphene.Boolean()
class SyncOptionsInput(graphene.InputObjectType):
"""Input for sync options."""
direction = String()
conflict_resolution = String()
dry_run = graphene.Boolean()
# Payload Types
class CreateSpacePayload(ObjectType):
"""Payload for createSpace mutation."""
space = Field(InformationSpaceType, description="The created space")
success = graphene.Boolean(description="Whether operation succeeded")
errors = List(String, description="Error messages")
class UpdateSpacePayload(ObjectType):
"""Payload for updateSpace mutation."""
space = Field(InformationSpaceType, description="The updated space")
success = graphene.Boolean(description="Whether operation succeeded")
errors = List(String, description="Error messages")
class DeleteSpacePayload(ObjectType):
"""Payload for deleteSpace mutation."""
success = graphene.Boolean(description="Whether operation succeeded")
deleted_id = String(description="ID of deleted space")
errors = List(String, description="Error messages")
class AddDocumentPayload(ObjectType):
"""Payload for addDocument mutation."""
document = Field(SpaceDocumentType, description="The added document")
success = graphene.Boolean(description="Whether operation succeeded")
errors = List(String, description="Error messages")
class RemoveDocumentPayload(ObjectType):
"""Payload for removeDocument mutation."""
success = graphene.Boolean(description="Whether operation succeeded")
removed_path = String(description="Path of removed document")
errors = List(String, description="Error messages")
class SetVariablePayload(ObjectType):
"""Payload for setVariable mutation."""
variable = Field(SpaceVariableType, description="The set variable")
success = graphene.Boolean(description="Whether operation succeeded")
errors = List(String, description="Error messages")
class RenderDocumentPayload(ObjectType):
"""Payload for renderDocument mutation."""
result = Field(RenderResultType, description="Render result")
success = graphene.Boolean(description="Whether operation succeeded")
errors = List(String, description="Error messages")
class SyncSpacePayload(ObjectType):
"""Payload for syncSpace mutation."""
result = Field(SyncResultType, description="Sync result")
conflicts = List(SyncConflictType, description="Conflicts encountered")
success = graphene.Boolean(description="Whether operation succeeded")
errors = List(String, description="Error messages")
# Query Extension
class SpaceQuery(ObjectType):
"""Query extension for spaces."""
# Single space queries
space = Field(
InformationSpaceType,
id=String(description="Space ID"),
name=String(description="Space name"),
description="Get a specific space"
)
# List queries
spaces = List(
InformationSpaceType,
status=String(description="Filter by status"),
limit=Int(default_value=50, description="Maximum results"),
offset=Int(default_value=0, description="Pagination offset"),
description="List all spaces"
)
# Document queries
space_documents = List(
SpaceDocumentType,
space_id=String(required=True, description="Space ID"),
description="List documents in a space"
)
space_document = Field(
SpaceDocumentType,
space_id=String(required=True, description="Space ID"),
space_path=String(required=True, description="Document path"),
description="Get a specific document in a space"
)
# Variable queries
space_variables = List(
SpaceVariableType,
space_id=String(required=True, description="Space ID"),
scope=String(description="Filter by scope"),
description="List variables in a space"
)
space_variable = Field(
SpaceVariableType,
space_id=String(required=True, description="Space ID"),
name=String(required=True, description="Variable name"),
description="Get a specific variable"
)
# Mutation Extension
class SpaceMutation(ObjectType):
"""Mutation extension for spaces."""
# Space lifecycle
create_space = Field(
CreateSpacePayload,
input=graphene.Argument(CreateSpaceInput, required=True),
description="Create a new space"
)
update_space = Field(
UpdateSpacePayload,
id=String(required=True, description="Space ID"),
input=graphene.Argument(UpdateSpaceInput, required=True),
description="Update a space"
)
delete_space = Field(
DeleteSpacePayload,
id=String(required=True, description="Space ID"),
description="Delete a space"
)
activate_space = Field(
UpdateSpacePayload,
id=String(required=True, description="Space ID"),
description="Activate a draft space"
)
archive_space = Field(
UpdateSpacePayload,
id=String(required=True, description="Space ID"),
description="Archive a space"
)
# Document operations
add_document = Field(
AddDocumentPayload,
space_id=String(required=True, description="Space ID"),
input=graphene.Argument(AddDocumentInput, required=True),
description="Add a document to a space"
)
remove_document = Field(
RemoveDocumentPayload,
space_id=String(required=True, description="Space ID"),
space_path=String(required=True, description="Document path"),
description="Remove a document from a space"
)
# Variable operations
set_variable = Field(
SetVariablePayload,
space_id=String(required=True, description="Space ID"),
name=String(required=True, description="Variable name"),
value=JSONString(required=True, description="Variable value"),
scope=String(description="Variable scope"),
description="Set a variable in a space"
)
delete_variable = Field(
SetVariablePayload,
space_id=String(required=True, description="Space ID"),
name=String(required=True, description="Variable name"),
description="Delete a variable from a space"
)
# Rendering
render_document = Field(
RenderDocumentPayload,
space_id=String(required=True, description="Space ID"),
document_path=String(required=True, description="Document path"),
options=graphene.Argument(RenderOptionsInput),
description="Render a document to HTML"
)
render_space = Field(
RenderDocumentPayload,
space_id=String(required=True, description="Space ID"),
options=graphene.Argument(RenderOptionsInput),
description="Render all documents in a space"
)
# Sync operations
sync_space = Field(
SyncSpacePayload,
space_id=String(required=True, description="Space ID"),
directory=String(required=True, description="Directory path"),
options=graphene.Argument(SyncOptionsInput),
description="Sync space with directory"
)
export_space = Field(
SyncSpacePayload,
space_id=String(required=True, description="Space ID"),
directory=String(required=True, description="Export directory"),
description="Export space to directory"
)
import_directory = Field(
SyncSpacePayload,
space_id=String(required=True, description="Target space ID"),
directory=String(required=True, description="Directory to import"),
description="Import directory into space"
)
# Cache operations
invalidate_cache = Field(
graphene.Boolean,
space_id=String(required=True, description="Space ID"),
document_path=String(description="Optional specific document"),
description="Invalidate render cache"
)