2026-01-26 11:09:40 -05:00

294 lines
11 KiB
Python

from typing import cast
import strawberry
from strawberry.types import Info
from channels.db import database_sync_to_async
from core.graphql.pubsub import pubsub
from core.graphql.inputs.scope import (
ScopeInput, ScopeUpdateInput,
AreaInput, AreaUpdateInput,
TaskInput, TaskUpdateInput,
TaskCompletionInput, TaskCompletionUpdateInput,
)
from core.graphql.types.scope import (
ScopeType,
AreaType,
TaskType,
TaskCompletionType,
)
from core.models.scope import Scope, Area, Task, TaskCompletion
from core.models.session import ServiceSession
from core.graphql.utils import create_object, update_object, delete_object, _decode_global_id
from core.services.events import (
publish_scope_created, publish_scope_updated, publish_scope_deleted,
publish_area_created, publish_area_updated, publish_area_deleted,
publish_task_created, publish_task_updated, publish_task_deleted,
publish_task_completion_recorded,
)
@strawberry.type
class Mutation:
@strawberry.mutation(description="Create a new scope")
async def create_scope(self, input: ScopeInput, info: Info) -> ScopeType:
instance = await create_object(input, Scope)
await pubsub.publish("scope_created", instance.id)
# Publish event
profile = getattr(info.context.request, 'profile', None)
await publish_scope_created(
scope_id=str(instance.id),
account_id=str(instance.account_id),
triggered_by=profile
)
return cast(ScopeType, instance)
@strawberry.mutation(description="Update an existing scope")
async def update_scope(self, input: ScopeUpdateInput, info: Info) -> ScopeType:
instance = await update_object(input, Scope)
await pubsub.publish("scope_updated", instance.id)
# Publish event
profile = getattr(info.context.request, 'profile', None)
await publish_scope_updated(
scope_id=str(instance.id),
account_id=str(instance.account_id),
triggered_by=profile
)
return cast(ScopeType, instance)
@strawberry.mutation(description="Delete an existing scope")
async def delete_scope(self, id: strawberry.ID, info: Info) -> strawberry.ID:
def _delete_scope_sync(scope_id):
"""
Smart delete: soft-delete if sessions reference this scope, hard-delete otherwise.
Returns (account_id, action) where action is 'deleted' or 'deactivated'.
"""
pk = _decode_global_id(scope_id)
try:
scope = Scope.objects.get(pk=pk)
except Scope.DoesNotExist:
return None, None
account_id = scope.account_id
# Check if any service sessions reference this scope
has_sessions = ServiceSession.objects.filter(scope_id=pk).exists()
if has_sessions:
# Soft delete - deactivate the scope to preserve historical data
scope.is_active = False
scope.save(update_fields=['is_active'])
else:
# Hard delete - no sessions reference this scope
scope.delete()
return account_id, 'deactivated' if has_sessions else 'deleted'
account_id, action = await database_sync_to_async(_delete_scope_sync)(id)
if account_id is None:
raise ValueError(f"Scope with ID {id} does not exist")
await pubsub.publish("scope_deleted", id)
# Publish event
profile = getattr(info.context.request, 'profile', None)
await publish_scope_deleted(
scope_id=str(id),
account_id=str(account_id),
triggered_by=profile
)
return id
@strawberry.mutation(description="Create a new area")
async def create_area(self, input: AreaInput, info: Info) -> AreaType:
instance = await create_object(input, Area)
await pubsub.publish("area_created", instance.id)
# Publish event
profile = getattr(info.context.request, 'profile', None)
await publish_area_created(
area_id=str(instance.id),
scope_id=str(instance.scope_id),
triggered_by=profile
)
return cast(AreaType, instance)
@strawberry.mutation(description="Update an existing area")
async def update_area(self, input: AreaUpdateInput, info: Info) -> AreaType:
instance = await update_object(input, Area)
await pubsub.publish("area_updated", instance.id)
# Publish event
profile = getattr(info.context.request, 'profile', None)
await publish_area_updated(
area_id=str(instance.id),
scope_id=str(instance.scope_id),
triggered_by=profile
)
return cast(AreaType, instance)
@strawberry.mutation(description="Delete an existing area")
async def delete_area(self, id: strawberry.ID, info: Info) -> strawberry.ID:
def _delete_area_sync(area_id):
"""
Delete an area if no task completions reference its tasks.
Returns scope_id on success, raises ValueError if completions exist.
"""
pk = _decode_global_id(area_id)
try:
area = Area.objects.get(pk=pk)
except Area.DoesNotExist:
return None
# Check if any task completions reference tasks in this area
has_completions = TaskCompletion.objects.filter(task__area_id=pk).exists()
if has_completions:
raise ValueError(
"Cannot delete area: it contains tasks with recorded completions. "
"Deactivate the scope instead to preserve historical data."
)
scope_id = area.scope_id
area.delete()
return scope_id
scope_id = await database_sync_to_async(_delete_area_sync)(id)
if scope_id is None:
raise ValueError(f"Area with ID {id} does not exist")
await pubsub.publish("area_deleted", id)
# Publish event
profile = getattr(info.context.request, 'profile', None)
await publish_area_deleted(
area_id=str(id),
scope_id=str(scope_id),
triggered_by=profile
)
return id
@strawberry.mutation(description="Create a new task")
async def create_task(self, input: TaskInput, info: Info) -> TaskType:
instance = await create_object(input, Task)
await pubsub.publish("task_created", instance.id)
# Publish event
profile = getattr(info.context.request, 'profile', None)
await publish_task_created(
task_id=str(instance.id),
area_id=str(instance.area_id),
triggered_by=profile
)
return cast(TaskType, instance)
@strawberry.mutation(description="Update an existing task")
async def update_task(self, input: TaskUpdateInput, info: Info) -> TaskType:
instance = await update_object(input, Task)
await pubsub.publish("task_updated", instance.id)
# Publish event
profile = getattr(info.context.request, 'profile', None)
await publish_task_updated(
task_id=str(instance.id),
area_id=str(instance.area_id),
triggered_by=profile
)
return cast(TaskType, instance)
@strawberry.mutation(description="Delete an existing task")
async def delete_task(self, id: strawberry.ID, info: Info) -> strawberry.ID:
def _delete_task_sync(task_id):
"""
Delete a task if no task completions reference it.
Returns area_id on success, raises ValueError if completions exist.
"""
pk = _decode_global_id(task_id)
try:
task = Task.objects.get(pk=pk)
except Task.DoesNotExist:
return None
# Check if any task completions reference this task
has_completions = TaskCompletion.objects.filter(task_id=pk).exists()
if has_completions:
raise ValueError(
"Cannot delete task: it has recorded completions. "
"Deactivate the scope instead to preserve historical data."
)
area_id = task.area_id
task.delete()
return area_id
area_id = await database_sync_to_async(_delete_task_sync)(id)
if area_id is None:
raise ValueError(f"Task with ID {id} does not exist")
await pubsub.publish("task_deleted", id)
# Publish event
profile = getattr(info.context.request, 'profile', None)
await publish_task_deleted(
task_id=str(id),
area_id=str(area_id),
triggered_by=profile
)
return id
@strawberry.mutation(description="Create a new task completion")
async def create_task_completion(self, input: TaskCompletionInput, info: Info) -> TaskCompletionType:
instance = await create_object(input, TaskCompletion)
await pubsub.publish("task_completion_created", instance.id)
# Publish event
profile = getattr(info.context.request, 'profile', None)
await publish_task_completion_recorded(
completion_id=str(instance.id),
task_id=str(instance.task_id),
service_id=str(instance.service_id),
triggered_by=profile
)
return cast(TaskCompletionType, instance)
@strawberry.mutation(description="Update an existing task completion")
async def update_task_completion(self, input: TaskCompletionUpdateInput, info: Info) -> TaskCompletionType:
instance = await update_object(input, TaskCompletion)
await pubsub.publish("task_completion_updated", instance.id)
# Publish event (reuse the same event for updates)
profile = getattr(info.context.request, 'profile', None)
await publish_task_completion_recorded(
completion_id=str(instance.id),
task_id=str(instance.task_id),
service_id=str(instance.service_id),
triggered_by=profile
)
return cast(TaskCompletionType, instance)
@strawberry.mutation(description="Delete an existing task completion")
async def delete_task_completion(self, id: strawberry.ID, info: Info) -> strawberry.ID:
instance = await delete_object(id, TaskCompletion)
if not instance:
raise ValueError(f"TaskCompletion with ID {id} does not exist")
await pubsub.publish("task_completion_deleted", id)
# Note: No event publication for deletion as there's no corresponding delete event
# in the events.py file for task completions
return id