""" Documents Handler - manages Appwrite document CRUD operations Phase I.1: 12 tools - Documents CRUD: 5 (list, get, create, update, delete) - Bulk Operations: 3 (bulk_create, bulk_update, bulk_delete) - Query/Count: 4 (search, count, get_by_query, list_with_cursor) """ import json from typing import Any from plugins.appwrite.client import AppwriteClient # ===================== # QUERY HELPERS (Appwrite 1.7.4 JSON format) # ===================== def _query_limit(value: int) -> str: """Build limit query in JSON format.""" return json.dumps({"method": "limit", "values": [value]}) def _query_offset(value: int) -> str: """Build offset query in JSON format.""" return json.dumps({"method": "offset", "values": [value]}) def _query_order_asc(attribute: str) -> str: """Build orderAsc query in JSON format.""" return json.dumps({"method": "orderAsc", "values": [attribute]}) def _query_order_desc(attribute: str) -> str: """Build orderDesc query in JSON format.""" return json.dumps({"method": "orderDesc", "values": [attribute]}) def _query_cursor_after(document_id: str) -> str: """Build cursorAfter query in JSON format.""" return json.dumps({"method": "cursorAfter", "values": [document_id]}) def _query_cursor_before(document_id: str) -> str: """Build cursorBefore query in JSON format.""" return json.dumps({"method": "cursorBefore", "values": [document_id]}) def _query_search(attribute: str, value: str) -> str: """Build search query in JSON format.""" return json.dumps({"method": "search", "attribute": attribute, "values": [value]}) def _query_equal(attribute: str, values: list[Any]) -> str: """Build equal query in JSON format.""" return json.dumps({"method": "equal", "attribute": attribute, "values": values}) def get_tool_specifications() -> list[dict[str, Any]]: """Return tool specifications for ToolGenerator (12 tools)""" return [ # ===================== # DOCUMENT CRUD (5) # ===================== { "name": "list_documents", "method_name": "list_documents", "description": """List documents in a collection with powerful query support. Queries must be JSON objects (Appwrite 1.7.4 format): - {"method": "equal", "attribute": "status", "values": ["active"]} - {"method": "notEqual", "attribute": "type", "values": ["draft"]} - {"method": "greaterThan", "attribute": "price", "values": [100]} - {"method": "search", "attribute": "title", "values": ["keyword"]} - {"method": "orderAsc", "values": ["createdAt"]} - {"method": "orderDesc", "values": ["createdAt"]} - {"method": "limit", "values": [25]} - {"method": "offset", "values": [50]} - {"method": "cursorAfter", "values": ["documentId"]}""", "schema": { "type": "object", "properties": { "database_id": {"type": "string", "description": "Database ID"}, "collection_id": {"type": "string", "description": "Collection ID"}, "queries": { "anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}], "description": "Query JSON strings for filtering, sorting, and pagination", "examples": [ [ '{"method":"equal","attribute":"status","values":["active"]}', '{"method":"limit","values":[25]}', ] ], }, }, "required": ["database_id", "collection_id"], }, "scope": "read", }, { "name": "get_document", "method_name": "get_document", "description": "Get a single document by ID.", "schema": { "type": "object", "properties": { "database_id": {"type": "string", "description": "Database ID"}, "collection_id": {"type": "string", "description": "Collection ID"}, "document_id": {"type": "string", "description": "Document ID"}, "queries": { "anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}], "description": "Optional queries for selecting specific attributes", }, }, "required": ["database_id", "collection_id", "document_id"], }, "scope": "read", }, { "name": "create_document", "method_name": "create_document", "description": "Create a new document. Use 'unique()' as document_id for auto-generated ID.", "schema": { "type": "object", "properties": { "database_id": {"type": "string", "description": "Database ID"}, "collection_id": {"type": "string", "description": "Collection ID"}, "document_id": { "type": "string", "description": "Unique document ID. Use 'unique()' for auto-generation", }, "data": { "type": "object", "description": "Document data (key-value pairs matching collection attributes)", }, "permissions": { "anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}], "description": "Document permissions (e.g., 'read(\"user:123\")', 'write(\"team:456\")')", }, }, "required": ["database_id", "collection_id", "document_id", "data"], }, "scope": "write", }, { "name": "update_document", "method_name": "update_document", "description": "Update an existing document. Only provided fields will be updated.", "schema": { "type": "object", "properties": { "database_id": {"type": "string", "description": "Database ID"}, "collection_id": {"type": "string", "description": "Collection ID"}, "document_id": {"type": "string", "description": "Document ID"}, "data": { "anyOf": [{"type": "object"}, {"type": "null"}], "description": "Fields to update", }, "permissions": { "anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}], "description": "New permissions (replaces existing)", }, }, "required": ["database_id", "collection_id", "document_id"], }, "scope": "write", }, { "name": "delete_document", "method_name": "delete_document", "description": "Delete a document by ID.", "schema": { "type": "object", "properties": { "database_id": {"type": "string", "description": "Database ID"}, "collection_id": {"type": "string", "description": "Collection ID"}, "document_id": {"type": "string", "description": "Document ID to delete"}, }, "required": ["database_id", "collection_id", "document_id"], }, "scope": "write", }, # ===================== # BULK OPERATIONS (3) # ===================== { "name": "bulk_create_documents", "method_name": "bulk_create_documents", "description": """Create multiple documents at once. More efficient than creating one by one. Example documents array: [ {"document_id": "unique()", "data": {"title": "Doc 1", "status": "active"}}, {"document_id": "doc-2", "data": {"title": "Doc 2", "status": "draft"}, "permissions": ["read(\\"any\\")"]} ] Each document must have: - document_id: Use 'unique()' for auto-generated ID or provide your own - data: Object with field values matching collection attributes""", "schema": { "type": "object", "properties": { "database_id": {"type": "string", "description": "Database ID"}, "collection_id": {"type": "string", "description": "Collection ID"}, "documents": { "type": "array", "items": { "type": "object", "properties": { "document_id": { "type": "string", "description": "Document ID. Use 'unique()' for auto-generation", }, "data": { "type": "object", "description": "Document data (key-value pairs matching collection attributes)", }, "permissions": { "anyOf": [ {"type": "array", "items": {"type": "string"}}, {"type": "null"}, ], "description": "Optional permissions for this document", }, }, "required": ["document_id", "data"], }, "description": "Array of documents to create. Each must have document_id and data.", }, }, "required": ["database_id", "collection_id", "documents"], }, "scope": "write", }, { "name": "bulk_update_documents", "method_name": "bulk_update_documents", "description": "Update multiple documents at once.", "schema": { "type": "object", "properties": { "database_id": {"type": "string", "description": "Database ID"}, "collection_id": {"type": "string", "description": "Collection ID"}, "updates": { "type": "array", "items": { "type": "object", "properties": { "document_id": {"type": "string"}, "data": {"type": "object"}, "permissions": { "anyOf": [ {"type": "array", "items": {"type": "string"}}, {"type": "null"}, ] }, }, "required": ["document_id", "data"], }, "description": "Array of document updates", }, }, "required": ["database_id", "collection_id", "updates"], }, "scope": "write", }, { "name": "bulk_delete_documents", "method_name": "bulk_delete_documents", "description": "Delete multiple documents at once.", "schema": { "type": "object", "properties": { "database_id": {"type": "string", "description": "Database ID"}, "collection_id": {"type": "string", "description": "Collection ID"}, "document_ids": { "type": "array", "items": {"type": "string"}, "description": "Array of document IDs to delete", }, }, "required": ["database_id", "collection_id", "document_ids"], }, "scope": "write", }, # ===================== # QUERY/SEARCH (4) # ===================== { "name": "search_documents", "method_name": "search_documents", "description": "Full-text search in documents. Requires a fulltext index on the searched attribute.", "schema": { "type": "object", "properties": { "database_id": {"type": "string", "description": "Database ID"}, "collection_id": {"type": "string", "description": "Collection ID"}, "attribute": { "type": "string", "description": "Attribute name to search in (must have fulltext index)", }, "query": {"type": "string", "description": "Search query string"}, "limit": {"type": "integer", "description": "Maximum results", "default": 25}, }, "required": ["database_id", "collection_id", "attribute", "query"], }, "scope": "read", }, { "name": "count_documents", "method_name": "count_documents", "description": "Count documents in a collection with optional filters.", "schema": { "type": "object", "properties": { "database_id": {"type": "string", "description": "Database ID"}, "collection_id": {"type": "string", "description": "Collection ID"}, "queries": { "anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}], "description": "Optional filter queries", }, }, "required": ["database_id", "collection_id"], }, "scope": "read", }, { "name": "get_documents_by_ids", "method_name": "get_documents_by_ids", "description": "Get multiple documents by their IDs.", "schema": { "type": "object", "properties": { "database_id": {"type": "string", "description": "Database ID"}, "collection_id": {"type": "string", "description": "Collection ID"}, "document_ids": { "type": "array", "items": {"type": "string"}, "description": "Array of document IDs to fetch", }, }, "required": ["database_id", "collection_id", "document_ids"], }, "scope": "read", }, { "name": "list_documents_paginated", "method_name": "list_documents_paginated", "description": "List documents with cursor-based pagination for efficient large dataset traversal.", "schema": { "type": "object", "properties": { "database_id": {"type": "string", "description": "Database ID"}, "collection_id": {"type": "string", "description": "Collection ID"}, "limit": { "type": "integer", "description": "Results per page", "default": 25, "maximum": 100, }, "cursor": { "anyOf": [{"type": "string"}, {"type": "null"}], "description": "Cursor from previous response for next page", }, "cursor_direction": { "type": "string", "enum": ["after", "before"], "description": "Cursor direction", "default": "after", }, "order_attribute": { "anyOf": [{"type": "string"}, {"type": "null"}], "description": "Attribute to order by (default: $id)", }, "order_type": { "type": "string", "enum": ["ASC", "DESC"], "description": "Sort order", "default": "DESC", }, "filters": { "anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}], "description": "Additional filter queries", }, }, "required": ["database_id", "collection_id"], }, "scope": "read", }, ] # ===================== # HANDLER FUNCTIONS # ===================== async def list_documents( client: AppwriteClient, database_id: str, collection_id: str, queries: list[str] | None = None ) -> str: """List documents with queries.""" try: result = await client.list_documents( database_id=database_id, collection_id=collection_id, queries=queries ) documents = result.get("documents", []) response = { "success": True, "database_id": database_id, "collection_id": collection_id, "total": result.get("total", len(documents)), "documents": documents, } return json.dumps(response, indent=2, ensure_ascii=False) except Exception as e: return json.dumps({"success": False, "error": str(e)}, indent=2) async def get_document( client: AppwriteClient, database_id: str, collection_id: str, document_id: str, queries: list[str] | None = None, ) -> str: """Get document by ID.""" try: result = await client.get_document( database_id=database_id, collection_id=collection_id, document_id=document_id, queries=queries, ) return json.dumps({"success": True, "document": result}, indent=2, ensure_ascii=False) except Exception as e: return json.dumps({"success": False, "error": str(e)}, indent=2) async def create_document( client: AppwriteClient, database_id: str, collection_id: str, document_id: str, data: dict[str, Any], permissions: list[str] | None = None, ) -> str: """Create a new document.""" try: result = await client.create_document( database_id=database_id, collection_id=collection_id, document_id=document_id, data=data, permissions=permissions, ) return json.dumps( {"success": True, "message": "Document created successfully", "document": result}, indent=2, ensure_ascii=False, ) except Exception as e: return json.dumps({"success": False, "error": str(e)}, indent=2) async def update_document( client: AppwriteClient, database_id: str, collection_id: str, document_id: str, data: dict[str, Any] | None = None, permissions: list[str] | None = None, ) -> str: """Update document.""" try: result = await client.update_document( database_id=database_id, collection_id=collection_id, document_id=document_id, data=data, permissions=permissions, ) return json.dumps( {"success": True, "message": "Document updated successfully", "document": result}, indent=2, ensure_ascii=False, ) except Exception as e: return json.dumps({"success": False, "error": str(e)}, indent=2) async def delete_document( client: AppwriteClient, database_id: str, collection_id: str, document_id: str ) -> str: """Delete document.""" try: await client.delete_document(database_id, collection_id, document_id) return json.dumps( {"success": True, "message": f"Document '{document_id}' deleted successfully"}, indent=2 ) except Exception as e: return json.dumps({"success": False, "error": str(e)}, indent=2) async def bulk_create_documents( client: AppwriteClient, database_id: str, collection_id: str, documents: list[dict[str, Any]] ) -> str: """Create multiple documents.""" try: results = [] errors = [] for doc in documents: try: result = await client.create_document( database_id=database_id, collection_id=collection_id, document_id=doc.get("document_id", "unique()"), data=doc["data"], permissions=doc.get("permissions"), ) results.append({"id": result.get("$id"), "success": True}) except Exception as e: errors.append({"document_id": doc.get("document_id"), "error": str(e)}) response = { "success": len(errors) == 0, "created": len(results), "failed": len(errors), "results": results, } if errors: response["errors"] = errors return json.dumps(response, indent=2, ensure_ascii=False) except Exception as e: return json.dumps({"success": False, "error": str(e)}, indent=2) async def bulk_update_documents( client: AppwriteClient, database_id: str, collection_id: str, updates: list[dict[str, Any]] ) -> str: """Update multiple documents.""" try: results = [] errors = [] for update in updates: try: result = await client.update_document( database_id=database_id, collection_id=collection_id, document_id=update["document_id"], data=update.get("data"), permissions=update.get("permissions"), ) results.append({"id": result.get("$id"), "success": True}) except Exception as e: errors.append({"document_id": update["document_id"], "error": str(e)}) response = { "success": len(errors) == 0, "updated": len(results), "failed": len(errors), "results": results, } if errors: response["errors"] = errors return json.dumps(response, indent=2, ensure_ascii=False) except Exception as e: return json.dumps({"success": False, "error": str(e)}, indent=2) async def bulk_delete_documents( client: AppwriteClient, database_id: str, collection_id: str, document_ids: list[str] ) -> str: """Delete multiple documents.""" try: results = [] errors = [] for doc_id in document_ids: try: await client.delete_document(database_id, collection_id, doc_id) results.append({"id": doc_id, "success": True}) except Exception as e: errors.append({"document_id": doc_id, "error": str(e)}) response = { "success": len(errors) == 0, "deleted": len(results), "failed": len(errors), "results": results, } if errors: response["errors"] = errors return json.dumps(response, indent=2, ensure_ascii=False) except Exception as e: return json.dumps({"success": False, "error": str(e)}, indent=2) async def search_documents( client: AppwriteClient, database_id: str, collection_id: str, attribute: str, query: str, limit: int = 25, ) -> str: """Full-text search in documents.""" try: # Appwrite 1.7.4 JSON format for queries queries = [_query_search(attribute, query), _query_limit(limit)] result = await client.list_documents( database_id=database_id, collection_id=collection_id, queries=queries ) documents = result.get("documents", []) response = { "success": True, "query": query, "attribute": attribute, "total": result.get("total", len(documents)), "documents": documents, } return json.dumps(response, indent=2, ensure_ascii=False) except Exception as e: error_msg = str(e) if "fulltext" in error_msg.lower() or "index" in error_msg.lower(): error_msg += " (Hint: Make sure you have a fulltext index on the searched attribute)" return json.dumps({"success": False, "error": error_msg}, indent=2) async def count_documents( client: AppwriteClient, database_id: str, collection_id: str, queries: list[str] | None = None ) -> str: """Count documents in collection.""" try: # Build query list: start with user filters, add limit(1) to minimize data count_queries = [] # Add user-provided filter queries (make a copy to avoid mutating input) if queries and len(queries) > 0: count_queries.extend(queries) # Always add limit(1) to minimize data transfer - Appwrite 1.7.4 JSON format count_queries.append(_query_limit(1)) result = await client.list_documents( database_id=database_id, collection_id=collection_id, queries=count_queries ) response = { "success": True, "database_id": database_id, "collection_id": collection_id, "count": result.get("total", 0), } return json.dumps(response, indent=2, ensure_ascii=False) except Exception as e: return json.dumps({"success": False, "error": str(e)}, indent=2) async def get_documents_by_ids( client: AppwriteClient, database_id: str, collection_id: str, document_ids: list[str] ) -> str: """Get multiple documents by IDs.""" try: documents = [] errors = [] for doc_id in document_ids: try: doc = await client.get_document( database_id=database_id, collection_id=collection_id, document_id=doc_id ) documents.append(doc) except Exception as e: errors.append({"document_id": doc_id, "error": str(e)}) response = { "success": len(errors) == 0, "found": len(documents), "not_found": len(errors), "documents": documents, } if errors: response["errors"] = errors return json.dumps(response, indent=2, ensure_ascii=False) except Exception as e: return json.dumps({"success": False, "error": str(e)}, indent=2) async def list_documents_paginated( client: AppwriteClient, database_id: str, collection_id: str, limit: int = 25, cursor: str | None = None, cursor_direction: str = "after", order_attribute: str | None = None, order_type: str = "DESC", filters: list[str] | None = None, ) -> str: """List documents with cursor pagination.""" try: # Build query list (don't mutate input) - Appwrite 1.7.4 JSON format queries = [] # Add user-provided filter queries if filters and len(filters) > 0: queries.extend(filters) # Add ordering only if explicitly specified if order_attribute: if order_type == "DESC": queries.append(_query_order_desc(order_attribute)) else: queries.append(_query_order_asc(order_attribute)) # Add cursor if cursor: if cursor_direction == "after": queries.append(_query_cursor_after(cursor)) else: queries.append(_query_cursor_before(cursor)) # Add limit only if queries exist or limit differs from default if queries or limit != 25: queries.append(_query_limit(min(limit, 100))) result = await client.list_documents( database_id=database_id, collection_id=collection_id, queries=queries if queries else None, ) documents = result.get("documents", []) # Determine next cursor next_cursor = None if documents and len(documents) == limit: next_cursor = documents[-1].get("$id") response = { "success": True, "total": result.get("total", 0), "count": len(documents), "documents": documents, "pagination": { "limit": limit, "cursor": cursor, "next_cursor": next_cursor, "has_more": next_cursor is not None, }, } return json.dumps(response, indent=2, ensure_ascii=False) except Exception as e: return json.dumps({"success": False, "error": str(e)}, indent=2)