- Fix Python formatting (sync no longer strips blank lines from .py files) - Remove test_community_build.py (tests private sync module) - Fix ruff warnings in test files - Add CODE_OF_CONDUCT.md - Add .github/PULL_REQUEST_TEMPLATE.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
592 lines
21 KiB
Python
592 lines
21 KiB
Python
"""
|
|
Items Handler - CRUD operations for any collection
|
|
|
|
Phase J.1: 12 tools
|
|
- list_items, get_item, create_item, create_items
|
|
- update_item, update_items, delete_item, delete_items
|
|
- search_items, aggregate_items, export_items, import_items
|
|
"""
|
|
|
|
import json
|
|
from typing import Any
|
|
|
|
from plugins.directus.client import DirectusClient
|
|
|
|
|
|
def _parse_json_param(value: Any, param_name: str = "parameter") -> Any:
|
|
"""Parse a parameter that may be a JSON string or already a native type."""
|
|
if value is None:
|
|
return None
|
|
if isinstance(value, (dict, list)):
|
|
return value
|
|
if isinstance(value, str):
|
|
stripped = value.strip()
|
|
if stripped.startswith("{") or stripped.startswith("["):
|
|
try:
|
|
return json.loads(stripped)
|
|
except json.JSONDecodeError as e:
|
|
raise ValueError(f"Invalid JSON in '{param_name}': {e}")
|
|
return value
|
|
|
|
|
|
def get_tool_specifications() -> list[dict[str, Any]]:
|
|
"""Return tool specifications for ToolGenerator (12 tools)"""
|
|
return [
|
|
{
|
|
"name": "list_items",
|
|
"method_name": "list_items",
|
|
"description": "List items from any Directus collection with filtering, sorting, and pagination.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"collection": {
|
|
"type": "string",
|
|
"description": "Collection name (e.g., 'posts', 'products')",
|
|
},
|
|
"fields": {
|
|
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
|
"description": "Fields to return (e.g., ['id', 'title', 'author.*'])",
|
|
},
|
|
"filter": {
|
|
"anyOf": [{"type": "object"}, {"type": "null"}],
|
|
"description": 'Filter object (e.g., {"status": {"_eq": "published"}})',
|
|
},
|
|
"sort": {
|
|
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
|
"description": "Sort fields (e.g., ['-date_created', 'title'])",
|
|
},
|
|
"limit": {
|
|
"type": "integer",
|
|
"description": "Maximum items to return",
|
|
"default": 100,
|
|
},
|
|
"offset": {"type": "integer", "description": "Items to skip", "default": 0},
|
|
"search": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Full-text search query",
|
|
},
|
|
},
|
|
"required": ["collection"],
|
|
},
|
|
"scope": "read",
|
|
},
|
|
{
|
|
"name": "get_item",
|
|
"method_name": "get_item",
|
|
"description": "Get a single item by ID from any collection.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"collection": {"type": "string", "description": "Collection name"},
|
|
"id": {"type": "string", "description": "Item ID"},
|
|
"fields": {
|
|
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
|
"description": "Fields to return",
|
|
},
|
|
},
|
|
"required": ["collection", "id"],
|
|
},
|
|
"scope": "read",
|
|
},
|
|
{
|
|
"name": "create_item",
|
|
"method_name": "create_item",
|
|
"description": "Create a new item in any collection.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"collection": {"type": "string", "description": "Collection name"},
|
|
"data": {"type": "object", "description": "Item data (field values)"},
|
|
"fields": {
|
|
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
|
"description": "Fields to return in response",
|
|
},
|
|
},
|
|
"required": ["collection", "data"],
|
|
},
|
|
"scope": "write",
|
|
},
|
|
{
|
|
"name": "create_items",
|
|
"method_name": "create_items",
|
|
"description": "Create multiple items in a collection at once.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"collection": {"type": "string", "description": "Collection name"},
|
|
"data": {
|
|
"type": "array",
|
|
"items": {"type": "object"},
|
|
"description": "Array of item data objects",
|
|
},
|
|
"fields": {
|
|
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
|
"description": "Fields to return in response",
|
|
},
|
|
},
|
|
"required": ["collection", "data"],
|
|
},
|
|
"scope": "write",
|
|
},
|
|
{
|
|
"name": "update_item",
|
|
"method_name": "update_item",
|
|
"description": "Update an existing item by ID.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"collection": {"type": "string", "description": "Collection name"},
|
|
"id": {"type": "string", "description": "Item ID"},
|
|
"data": {"type": "object", "description": "Fields to update"},
|
|
"fields": {
|
|
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
|
"description": "Fields to return in response",
|
|
},
|
|
},
|
|
"required": ["collection", "id", "data"],
|
|
},
|
|
"scope": "write",
|
|
},
|
|
{
|
|
"name": "update_items",
|
|
"method_name": "update_items",
|
|
"description": "Update multiple items by their IDs.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"collection": {"type": "string", "description": "Collection name"},
|
|
"keys": {
|
|
"type": "array",
|
|
"items": {"type": "string"},
|
|
"description": "Array of item IDs to update",
|
|
},
|
|
"data": {
|
|
"type": "object",
|
|
"description": "Fields to update (applied to all items)",
|
|
},
|
|
"fields": {
|
|
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
|
"description": "Fields to return in response",
|
|
},
|
|
},
|
|
"required": ["collection", "keys", "data"],
|
|
},
|
|
"scope": "write",
|
|
},
|
|
{
|
|
"name": "delete_item",
|
|
"method_name": "delete_item",
|
|
"description": "Delete an item by ID. This action is irreversible.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"collection": {"type": "string", "description": "Collection name"},
|
|
"id": {"type": "string", "description": "Item ID to delete"},
|
|
},
|
|
"required": ["collection", "id"],
|
|
},
|
|
"scope": "admin",
|
|
},
|
|
{
|
|
"name": "delete_items",
|
|
"method_name": "delete_items",
|
|
"description": "Delete multiple items by their IDs. This action is irreversible.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"collection": {"type": "string", "description": "Collection name"},
|
|
"keys": {
|
|
"type": "array",
|
|
"items": {"type": "string"},
|
|
"description": "Array of item IDs to delete",
|
|
},
|
|
},
|
|
"required": ["collection", "keys"],
|
|
},
|
|
"scope": "admin",
|
|
},
|
|
{
|
|
"name": "search_items",
|
|
"method_name": "search_items",
|
|
"description": "Full-text search across items in a collection.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"collection": {"type": "string", "description": "Collection name"},
|
|
"query": {"type": "string", "description": "Search query"},
|
|
"fields": {
|
|
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
|
"description": "Fields to return",
|
|
},
|
|
"limit": {
|
|
"type": "integer",
|
|
"description": "Maximum items to return",
|
|
"default": 25,
|
|
},
|
|
},
|
|
"required": ["collection", "query"],
|
|
},
|
|
"scope": "read",
|
|
},
|
|
{
|
|
"name": "aggregate_items",
|
|
"method_name": "aggregate_items",
|
|
"description": "Perform aggregate operations (count, sum, avg, min, max) on items.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"collection": {"type": "string", "description": "Collection name"},
|
|
"aggregate": {
|
|
"type": "object",
|
|
"description": 'Aggregate functions (e.g., {"count": "*", "sum": "price"})',
|
|
},
|
|
"filter": {
|
|
"anyOf": [{"type": "object"}, {"type": "null"}],
|
|
"description": "Filter to apply before aggregation",
|
|
},
|
|
"groupBy": {
|
|
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
|
"description": "Fields to group by",
|
|
},
|
|
},
|
|
"required": ["collection", "aggregate"],
|
|
},
|
|
"scope": "read",
|
|
},
|
|
{
|
|
"name": "export_items",
|
|
"method_name": "export_items",
|
|
"description": "Export items from a collection with filtering options.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"collection": {"type": "string", "description": "Collection name"},
|
|
"fields": {
|
|
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
|
"description": "Fields to export",
|
|
},
|
|
"filter": {
|
|
"anyOf": [{"type": "object"}, {"type": "null"}],
|
|
"description": "Filter to apply",
|
|
},
|
|
"sort": {
|
|
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
|
"description": "Sort fields",
|
|
},
|
|
"limit": {
|
|
"type": "integer",
|
|
"description": "Maximum items to export",
|
|
"default": 1000,
|
|
},
|
|
},
|
|
"required": ["collection"],
|
|
},
|
|
"scope": "read",
|
|
},
|
|
{
|
|
"name": "import_items",
|
|
"method_name": "import_items",
|
|
"description": "Import multiple items into a collection.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"collection": {"type": "string", "description": "Collection name"},
|
|
"data": {
|
|
"type": "array",
|
|
"items": {"type": "object"},
|
|
"description": "Array of items to import",
|
|
},
|
|
},
|
|
"required": ["collection", "data"],
|
|
},
|
|
"scope": "write",
|
|
},
|
|
]
|
|
|
|
|
|
# =====================
|
|
# HANDLER FUNCTIONS
|
|
# =====================
|
|
|
|
|
|
async def list_items(
|
|
client: DirectusClient,
|
|
collection: str,
|
|
fields: list[str] | None = None,
|
|
filter: dict | None = None,
|
|
sort: list[str] | None = None,
|
|
limit: int = 100,
|
|
offset: int = 0,
|
|
search: str | None = None,
|
|
) -> str:
|
|
"""List items from a collection."""
|
|
try:
|
|
# Parse JSON string parameters
|
|
parsed_fields = _parse_json_param(fields, "fields")
|
|
parsed_filter = _parse_json_param(filter, "filter")
|
|
parsed_sort = _parse_json_param(sort, "sort")
|
|
|
|
result = await client.list_items(
|
|
collection=collection,
|
|
fields=parsed_fields,
|
|
filter=parsed_filter,
|
|
sort=parsed_sort,
|
|
limit=limit,
|
|
offset=offset,
|
|
search=search,
|
|
)
|
|
items = result.get("data", [])
|
|
return json.dumps(
|
|
{"success": True, "collection": collection, "total": len(items), "items": items},
|
|
indent=2,
|
|
ensure_ascii=False,
|
|
)
|
|
except Exception as e:
|
|
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
|
|
|
|
|
async def get_item(
|
|
client: DirectusClient, collection: str, id: str, fields: list[str] | None = None
|
|
) -> str:
|
|
"""Get item by ID."""
|
|
try:
|
|
# Parse JSON string parameter
|
|
parsed_fields = _parse_json_param(fields, "fields")
|
|
result = await client.get_item(collection, id, fields=parsed_fields)
|
|
return json.dumps(
|
|
{"success": True, "item": result.get("data")}, indent=2, ensure_ascii=False
|
|
)
|
|
except Exception as e:
|
|
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
|
|
|
|
|
async def create_item(
|
|
client: DirectusClient, collection: str, data: dict[str, Any], fields: list[str] | None = None
|
|
) -> str:
|
|
"""Create a new item."""
|
|
try:
|
|
# Parse JSON string parameters
|
|
parsed_data = _parse_json_param(data, "data")
|
|
parsed_fields = _parse_json_param(fields, "fields")
|
|
result = await client.create_item(collection, parsed_data, fields=parsed_fields)
|
|
return json.dumps(
|
|
{
|
|
"success": True,
|
|
"message": f"Item created in {collection}",
|
|
"item": result.get("data"),
|
|
},
|
|
indent=2,
|
|
ensure_ascii=False,
|
|
)
|
|
except Exception as e:
|
|
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
|
|
|
|
|
async def create_items(
|
|
client: DirectusClient,
|
|
collection: str,
|
|
data: list[dict[str, Any]],
|
|
fields: list[str] | None = None,
|
|
) -> str:
|
|
"""Create multiple items."""
|
|
try:
|
|
# Parse JSON string parameters
|
|
parsed_data = _parse_json_param(data, "data")
|
|
parsed_fields = _parse_json_param(fields, "fields")
|
|
result = await client.create_items(collection, parsed_data, fields=parsed_fields)
|
|
items = result.get("data", [])
|
|
return json.dumps(
|
|
{
|
|
"success": True,
|
|
"message": f"Created {len(items)} items in {collection}",
|
|
"items": items,
|
|
},
|
|
indent=2,
|
|
ensure_ascii=False,
|
|
)
|
|
except Exception as e:
|
|
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
|
|
|
|
|
async def update_item(
|
|
client: DirectusClient,
|
|
collection: str,
|
|
id: str,
|
|
data: dict[str, Any],
|
|
fields: list[str] | None = None,
|
|
) -> str:
|
|
"""Update an item."""
|
|
try:
|
|
# Parse JSON string parameters
|
|
parsed_data = _parse_json_param(data, "data")
|
|
parsed_fields = _parse_json_param(fields, "fields")
|
|
result = await client.update_item(collection, id, parsed_data, fields=parsed_fields)
|
|
return json.dumps(
|
|
{"success": True, "message": f"Item {id} updated", "item": result.get("data")},
|
|
indent=2,
|
|
ensure_ascii=False,
|
|
)
|
|
except Exception as e:
|
|
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
|
|
|
|
|
async def update_items(
|
|
client: DirectusClient,
|
|
collection: str,
|
|
keys: list[str],
|
|
data: dict[str, Any],
|
|
fields: list[str] | None = None,
|
|
) -> str:
|
|
"""Update multiple items."""
|
|
try:
|
|
# Parse JSON string parameters
|
|
parsed_keys = _parse_json_param(keys, "keys")
|
|
parsed_data = _parse_json_param(data, "data")
|
|
parsed_fields = _parse_json_param(fields, "fields")
|
|
result = await client.update_items(
|
|
collection, parsed_keys, parsed_data, fields=parsed_fields
|
|
)
|
|
items = result.get("data", [])
|
|
return json.dumps(
|
|
{
|
|
"success": True,
|
|
"message": f"Updated {len(items)} items in {collection}",
|
|
"items": items,
|
|
},
|
|
indent=2,
|
|
ensure_ascii=False,
|
|
)
|
|
except Exception as e:
|
|
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
|
|
|
|
|
async def delete_item(client: DirectusClient, collection: str, id: str) -> str:
|
|
"""Delete an item."""
|
|
try:
|
|
await client.delete_item(collection, id)
|
|
return json.dumps(
|
|
{"success": True, "message": f"Item {id} deleted from {collection}"}, indent=2
|
|
)
|
|
except Exception as e:
|
|
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
|
|
|
|
|
async def delete_items(client: DirectusClient, collection: str, keys: list[str]) -> str:
|
|
"""Delete multiple items."""
|
|
try:
|
|
# Parse JSON string parameter
|
|
parsed_keys = _parse_json_param(keys, "keys")
|
|
await client.delete_items(collection, parsed_keys)
|
|
return json.dumps(
|
|
{"success": True, "message": f"Deleted {len(keys)} items from {collection}"}, indent=2
|
|
)
|
|
except Exception as e:
|
|
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
|
|
|
|
|
async def search_items(
|
|
client: DirectusClient,
|
|
collection: str,
|
|
query: str,
|
|
fields: list[str] | None = None,
|
|
limit: int = 25,
|
|
) -> str:
|
|
"""Full-text search items."""
|
|
try:
|
|
# Parse JSON string parameter
|
|
parsed_fields = _parse_json_param(fields, "fields")
|
|
result = await client.list_items(
|
|
collection=collection, fields=parsed_fields, search=query, limit=limit
|
|
)
|
|
items = result.get("data", [])
|
|
return json.dumps(
|
|
{"success": True, "query": query, "total": len(items), "items": items},
|
|
indent=2,
|
|
ensure_ascii=False,
|
|
)
|
|
except Exception as e:
|
|
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
|
|
|
|
|
async def aggregate_items(
|
|
client: DirectusClient,
|
|
collection: str,
|
|
aggregate: dict[str, Any],
|
|
filter: dict | None = None,
|
|
groupBy: list[str] | None = None,
|
|
) -> str:
|
|
"""Aggregate items."""
|
|
try:
|
|
# Parse JSON string parameters
|
|
parsed_aggregate = _parse_json_param(aggregate, "aggregate")
|
|
parsed_filter = _parse_json_param(filter, "filter")
|
|
_parse_json_param(groupBy, "groupBy")
|
|
|
|
result = await client.list_items(
|
|
collection=collection, filter=parsed_filter, aggregate=parsed_aggregate
|
|
)
|
|
return json.dumps(
|
|
{"success": True, "collection": collection, "result": result.get("data")},
|
|
indent=2,
|
|
ensure_ascii=False,
|
|
)
|
|
except Exception as e:
|
|
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
|
|
|
|
|
async def export_items(
|
|
client: DirectusClient,
|
|
collection: str,
|
|
fields: list[str] | None = None,
|
|
filter: dict | None = None,
|
|
sort: list[str] | None = None,
|
|
limit: int = 1000,
|
|
) -> str:
|
|
"""Export items from a collection."""
|
|
try:
|
|
# Parse JSON string parameters
|
|
parsed_fields = _parse_json_param(fields, "fields")
|
|
parsed_filter = _parse_json_param(filter, "filter")
|
|
parsed_sort = _parse_json_param(sort, "sort")
|
|
|
|
result = await client.list_items(
|
|
collection=collection,
|
|
fields=parsed_fields,
|
|
filter=parsed_filter,
|
|
sort=parsed_sort,
|
|
limit=limit,
|
|
)
|
|
items = result.get("data", [])
|
|
return json.dumps(
|
|
{
|
|
"success": True,
|
|
"collection": collection,
|
|
"exported_count": len(items),
|
|
"data": items,
|
|
},
|
|
indent=2,
|
|
ensure_ascii=False,
|
|
)
|
|
except Exception as e:
|
|
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
|
|
|
|
|
async def import_items(client: DirectusClient, collection: str, data: list[dict[str, Any]]) -> str:
|
|
"""Import items into a collection."""
|
|
try:
|
|
# Parse JSON string parameter
|
|
parsed_data = _parse_json_param(data, "data")
|
|
result = await client.create_items(collection, parsed_data)
|
|
items = result.get("data", [])
|
|
return json.dumps(
|
|
{
|
|
"success": True,
|
|
"message": f"Imported {len(items)} items into {collection}",
|
|
"imported_count": len(items),
|
|
},
|
|
indent=2,
|
|
ensure_ascii=False,
|
|
)
|
|
except Exception as e:
|
|
return json.dumps({"success": False, "error": str(e)}, indent=2)
|