""" GraphQL resolvers for Information Spaces. Implements resolver functions for space queries and mutations. """ from typing import Optional, Dict, Any, List from pathlib import Path from .space_schema import ( InformationSpaceType, SpaceDocumentType, SpaceVariableType, SpaceConfigType, SpaceMetadataType, RenderResultType, SyncResultType, SyncConflictType, CreateSpacePayload, UpdateSpacePayload, DeleteSpacePayload, AddDocumentPayload, RemoveDocumentPayload, SetVariablePayload, RenderDocumentPayload, SyncSpacePayload, ) def get_space_service(): """Get the space service instance.""" # This would be configured during app initialization # For now, we create a new instance from ..spaces.services.space_service import SpaceService from ..spaces.repositories.sqlite import ( SqliteSpaceRepository, SqliteDocumentRepository, SqliteVariableRepository, SqliteReferenceRepository, ) # Use default database path import os db_path = Path(os.path.expanduser("~/.markitect/spaces.db")) db_path.parent.mkdir(parents=True, exist_ok=True) return SpaceService( space_repo=SqliteSpaceRepository(db_path), document_repo=SqliteDocumentRepository(db_path), variable_repo=SqliteVariableRepository(db_path), reference_repo=SqliteReferenceRepository(db_path), ) def _space_to_graphql(space) -> InformationSpaceType: """Convert a space model to GraphQL type.""" config = None if space.config: config = SpaceConfigType( default_variant=space.config.default_variant, enable_caching=space.config.enable_caching, theme=space.config.theme, history_enabled=space.config.history_enabled, variable_scope=space.config.variable_scope, ) metadata = None if space.metadata: metadata = SpaceMetadataType( tags=space.metadata.tags if hasattr(space.metadata, 'tags') else [], author=space.metadata.author if hasattr(space.metadata, 'author') else None, custom=space.metadata.custom if hasattr(space.metadata, 'custom') else {}, ) return InformationSpaceType( id=space.id, name=space.name, description=space.description, status=space.status.value if hasattr(space.status, 'value') else str(space.status), config=config, metadata=metadata, parent_space_id=space.parent_space_id, created_at=space.created_at, updated_at=space.updated_at, ) def _document_to_graphql(doc) -> SpaceDocumentType: """Convert a document model to GraphQL type.""" return SpaceDocumentType( id=doc.id, space_id=doc.space_id, document_id=doc.document_id, space_path=doc.space_path, order_index=doc.order_index, metadata=doc.metadata, added_at=doc.added_at, ) def _variable_to_graphql(var) -> SpaceVariableType: """Convert a variable model to GraphQL type.""" return SpaceVariableType( space_id=var.space_id, name=var.name, value=var.value, scope=var.scope, ) # Query Resolvers def resolve_space(root, info, id=None, name=None): """Resolve a single space by ID or name.""" service = get_space_service() if id: space = service.get_space(id) elif name: space = service.get_space_by_name(name) else: return None if space: return _space_to_graphql(space) return None def resolve_spaces(root, info, status=None, limit=50, offset=0): """Resolve list of spaces.""" service = get_space_service() spaces = service.list_spaces(status=status, limit=limit, offset=offset) return [_space_to_graphql(s) for s in spaces] def resolve_space_documents(root, info, space_id): """Resolve documents in a space.""" service = get_space_service() documents = service.list_documents(space_id) return [_document_to_graphql(d) for d in documents] def resolve_space_document(root, info, space_id, space_path): """Resolve a specific document in a space.""" service = get_space_service() doc = service.get_document(space_id, space_path) if doc: return _document_to_graphql(doc) return None def resolve_space_variables(root, info, space_id, scope=None): """Resolve variables in a space.""" service = get_space_service() variables = service.list_variables(space_id, scope=scope) return [_variable_to_graphql(v) for v in variables] def resolve_space_variable(root, info, space_id, name): """Resolve a specific variable.""" service = get_space_service() var = service.get_variable(space_id, name) if var: return _variable_to_graphql(var) return None # Mutation Resolvers def resolve_create_space(root, info, input): """Create a new space.""" try: service = get_space_service() # Build config and metadata from input from ..spaces.models import SpaceConfig, SpaceMetadata config = SpaceConfig() if input.config: if input.config.default_variant: config.default_variant = input.config.default_variant if input.config.enable_caching is not None: config.enable_caching = input.config.enable_caching if input.config.theme: config.theme = input.config.theme if input.config.history_enabled is not None: config.history_enabled = input.config.history_enabled metadata = SpaceMetadata() if input.metadata: if input.metadata.tags: metadata.tags = input.metadata.tags if input.metadata.author: metadata.author = input.metadata.author if input.metadata.custom: metadata.custom = input.metadata.custom space = service.create_space( name=input.name, description=input.description, config=config, metadata=metadata, parent_space_id=input.parent_space_id, ) return CreateSpacePayload( space=_space_to_graphql(space), success=True, errors=[], ) except Exception as e: return CreateSpacePayload( space=None, success=False, errors=[str(e)], ) def resolve_update_space(root, info, id, input): """Update a space.""" try: service = get_space_service() updates = {} if input.name: updates["name"] = input.name if input.description: updates["description"] = input.description space = service.update_space(id, **updates) return UpdateSpacePayload( space=_space_to_graphql(space), success=True, errors=[], ) except Exception as e: return UpdateSpacePayload( space=None, success=False, errors=[str(e)], ) def resolve_delete_space(root, info, id): """Delete a space.""" try: service = get_space_service() service.delete_space(id) return DeleteSpacePayload( success=True, deleted_id=id, errors=[], ) except Exception as e: return DeleteSpacePayload( success=False, deleted_id=None, errors=[str(e)], ) def resolve_activate_space(root, info, id): """Activate a draft space.""" try: service = get_space_service() space = service.activate_space(id) return UpdateSpacePayload( space=_space_to_graphql(space), success=True, errors=[], ) except Exception as e: return UpdateSpacePayload( space=None, success=False, errors=[str(e)], ) def resolve_archive_space(root, info, id): """Archive a space.""" try: service = get_space_service() space = service.archive_space(id) return UpdateSpacePayload( space=_space_to_graphql(space), success=True, errors=[], ) except Exception as e: return UpdateSpacePayload( space=None, success=False, errors=[str(e)], ) def resolve_add_document(root, info, space_id, input): """Add a document to a space.""" try: service = get_space_service() doc = service.add_document( space_id=space_id, document_id=input.document_id, space_path=input.space_path, order_index=input.order_index or 0, metadata=input.metadata, ) return AddDocumentPayload( document=_document_to_graphql(doc), success=True, errors=[], ) except Exception as e: return AddDocumentPayload( document=None, success=False, errors=[str(e)], ) def resolve_remove_document(root, info, space_id, space_path): """Remove a document from a space.""" try: service = get_space_service() service.remove_document(space_id, space_path) return RemoveDocumentPayload( success=True, removed_path=space_path, errors=[], ) except Exception as e: return RemoveDocumentPayload( success=False, removed_path=None, errors=[str(e)], ) def resolve_set_variable(root, info, space_id, name, value, scope=None): """Set a variable in a space.""" try: service = get_space_service() var = service.set_variable( space_id=space_id, name=name, value=value, scope=scope or "space", ) return SetVariablePayload( variable=_variable_to_graphql(var), success=True, errors=[], ) except Exception as e: return SetVariablePayload( variable=None, success=False, errors=[str(e)], ) def resolve_delete_variable(root, info, space_id, name): """Delete a variable from a space.""" try: service = get_space_service() service.delete_variable(space_id, name) return SetVariablePayload( variable=None, success=True, errors=[], ) except Exception as e: return SetVariablePayload( variable=None, success=False, errors=[str(e)], ) def resolve_render_document(root, info, space_id, document_path, options=None): """Render a document to HTML.""" try: from ..spaces.rendering import SpaceRenderingService, RenderConfig, ThemeConfig service = get_space_service() doc = service.get_document(space_id, document_path) if not doc: return RenderDocumentPayload( result=None, success=False, errors=[f"Document not found: {document_path}"], ) # Get render config from options theme_name = "default" include_toc = False force_refresh = False if options: if options.theme: theme_name = options.theme if options.include_toc: include_toc = options.include_toc if options.force_refresh: force_refresh = options.force_refresh config = RenderConfig( theme=ThemeConfig(name=theme_name, layers=[theme_name]), include_toc=include_toc, ) renderer = SpaceRenderingService() # Would need actual content here result = renderer.render_document( content="# Placeholder", # Would get actual content document_id=doc.document_id, space_id=space_id, force_refresh=force_refresh, ) return RenderDocumentPayload( result=RenderResultType( content=result.content, format=result.format.value, content_hash=result.content_hash, source_hash=result.source_hash, document_id=result.document_id, cached=False, ), success=True, errors=[], ) except Exception as e: return RenderDocumentPayload( result=None, success=False, errors=[str(e)], ) def resolve_sync_space(root, info, space_id, directory, options=None): """Sync a space with a directory.""" try: from ..spaces.sync import ( BidirectionalSyncCoordinator, SyncConfig, SyncDirection, ConflictResolution, ) service = get_space_service() space = service.get_space(space_id) if not space: return SyncSpacePayload( result=None, conflicts=[], success=False, errors=[f"Space not found: {space_id}"], ) # Build sync config direction = SyncDirection.BIDIRECTIONAL conflict_res = ConflictResolution.NEWER_WINS dry_run = False if options: if options.direction: direction = SyncDirection(options.direction) if options.conflict_resolution: conflict_res = ConflictResolution(options.conflict_resolution) if options.dry_run: dry_run = options.dry_run config = SyncConfig( direction=direction, conflict_resolution=conflict_res, dry_run=dry_run, ) coordinator = BidirectionalSyncCoordinator(config) documents = service.list_documents(space_id) def content_provider(doc_id): return "" # Would get actual content result = coordinator.sync( space=space, documents=documents, content_provider=content_provider, directory=Path(directory), ) return SyncSpacePayload( result=SyncResultType( success=result.success, created_count=result.created_count, updated_count=result.updated_count, deleted_count=result.deleted_count, conflict_count=len(result.conflicts), errors=list(result.errors.values()), ), conflicts=[ SyncConflictType( path=c.path, space_hash=c.space_state.content_hash if c.space_state else None, directory_hash=c.directory_state.content_hash if c.directory_state else None, resolution=c.resolution.value if hasattr(c.resolution, 'value') else str(c.resolution), winner=c.winner, ) for c in result.conflicts ], success=result.success, errors=[], ) except Exception as e: return SyncSpacePayload( result=None, conflicts=[], success=False, errors=[str(e)], ) def resolve_invalidate_cache(root, info, space_id, document_path=None): """Invalidate render cache.""" try: from ..spaces.rendering import SpaceRenderingService service = SpaceRenderingService() if document_path: service.invalidate_document(document_path, space_id) else: service.invalidate_space(space_id) return True except Exception: return False