from typing import cast import strawberry from strawberry.types import Info from asgiref.sync import sync_to_async from core.graphql.pubsub import pubsub from core.graphql.utils import create_object, update_object, delete_object, _decode_global_id from core.graphql.types.scope_template import ( ScopeTemplateType, AreaTemplateType, TaskTemplateType, ) from core.graphql.types.scope import ScopeType from core.graphql.inputs.scope_template import ( ScopeTemplateInput, ScopeTemplateUpdateInput, AreaTemplateInput, AreaTemplateUpdateInput, TaskTemplateInput, TaskTemplateUpdateInput, CreateScopeFromTemplateInput, ) from core.models.scope_template import ScopeTemplate, AreaTemplate, TaskTemplate from core.models.account import Account, AccountAddress from strawberry.scalars import JSON from core.services import build_scope_template from core.services.events import ( publish_scope_template_created, publish_scope_template_updated, publish_scope_template_deleted, publish_scope_template_instantiated, publish_area_template_created, publish_area_template_updated, publish_area_template_deleted, publish_task_template_created, publish_task_template_updated, publish_task_template_deleted, ) @strawberry.type class Mutation: @strawberry.mutation(description="Create a new scope template") async def create_scope_template(self, input: ScopeTemplateInput, info: Info) -> ScopeTemplateType: instance = await create_object(input, ScopeTemplate) await pubsub.publish("scope_template_created", instance.id) # Publish event profile = getattr(info.context.request, 'profile', None) await publish_scope_template_created( template_id=str(instance.id), triggered_by=profile ) return cast(ScopeTemplateType, instance) @strawberry.mutation(description="Update an existing scope template") async def update_scope_template(self, input: ScopeTemplateUpdateInput, info: Info) -> ScopeTemplateType: instance = await update_object(input, ScopeTemplate) await pubsub.publish("scope_template_updated", instance.id) # Publish event profile = getattr(info.context.request, 'profile', None) await publish_scope_template_updated( template_id=str(instance.id), triggered_by=profile ) return cast(ScopeTemplateType, instance) @strawberry.mutation(description="Delete an existing scope template") async def delete_scope_template(self, id: strawberry.ID, info: Info) -> strawberry.ID: instance = await delete_object(id, ScopeTemplate) if not instance: raise ValueError(f"ScopeTemplate with ID {id} does not exist") await pubsub.publish("scope_template_deleted", id) # Publish event profile = getattr(info.context.request, 'profile', None) await publish_scope_template_deleted( template_id=str(id), triggered_by=profile ) return id @strawberry.mutation(description="Create a new area template") async def create_area_template(self, input: AreaTemplateInput, info: Info) -> AreaTemplateType: instance = await create_object(input, AreaTemplate) await pubsub.publish("area_template_created", instance.id) # Publish event profile = getattr(info.context.request, 'profile', None) await publish_area_template_created( template_id=str(instance.id), scope_template_id=str(instance.scope_template_id), triggered_by=profile ) return cast(AreaTemplateType, instance) @strawberry.mutation(description="Update an existing area template") async def update_area_template(self, input: AreaTemplateUpdateInput, info: Info) -> AreaTemplateType: instance = await update_object(input, AreaTemplate) await pubsub.publish("area_template_updated", instance.id) # Publish event profile = getattr(info.context.request, 'profile', None) await publish_area_template_updated( template_id=str(instance.id), scope_template_id=str(instance.scope_template_id), triggered_by=profile ) return cast(AreaTemplateType, instance) @strawberry.mutation(description="Delete an existing area template") async def delete_area_template(self, id: strawberry.ID, info: Info) -> strawberry.ID: instance = await delete_object(id, AreaTemplate) if not instance: raise ValueError(f"AreaTemplate with ID {id} does not exist") await pubsub.publish("area_template_deleted", id) # Publish event profile = getattr(info.context.request, 'profile', None) await publish_area_template_deleted( template_id=str(id), scope_template_id=str(instance.scope_template_id), triggered_by=profile ) return id @strawberry.mutation(description="Create a new task template") async def create_task_template(self, input: TaskTemplateInput, info: Info) -> TaskTemplateType: instance = await create_object(input, TaskTemplate) await pubsub.publish("task_template_created", instance.id) # Publish event profile = getattr(info.context.request, 'profile', None) await publish_task_template_created( template_id=str(instance.id), area_template_id=str(instance.area_template_id), triggered_by=profile ) return cast(TaskTemplateType, instance) @strawberry.mutation(description="Update an existing task template") async def update_task_template(self, input: TaskTemplateUpdateInput, info: Info) -> TaskTemplateType: instance = await update_object(input, TaskTemplate) await pubsub.publish("task_template_updated", instance.id) # Publish event profile = getattr(info.context.request, 'profile', None) await publish_task_template_updated( template_id=str(instance.id), area_template_id=str(instance.area_template_id), triggered_by=profile ) return cast(TaskTemplateType, instance) @strawberry.mutation(description="Delete an existing task template") async def delete_task_template(self, id: strawberry.ID, info: Info) -> strawberry.ID: instance = await delete_object(id, TaskTemplate) if not instance: raise ValueError(f"TaskTemplate with ID {id} does not exist") await pubsub.publish("task_template_deleted", id) # Publish event profile = getattr(info.context.request, 'profile', None) await publish_task_template_deleted( template_id=str(id), area_template_id=str(instance.area_template_id), triggered_by=profile ) return id @strawberry.mutation(description="Instantiate a Scope (with Areas and Tasks) from a ScopeTemplate") async def create_scope_from_template(self, input: CreateScopeFromTemplateInput, info: Info) -> ScopeType: def _do_create_sync(): template = ScopeTemplate.objects.get(pk=_decode_global_id(input.template_id)) account = Account.objects.get(pk=_decode_global_id(input.account_id)) account_address = None if input.account_address_id: account_address = AccountAddress.objects.get( pk=_decode_global_id(input.account_address_id), account=account ) scope = template.instantiate( account=account, account_address=account_address, name=input.name, description=input.description, is_active=input.is_active if input.is_active is not None else True, ) return scope, str(template.id), str(account.id) # Run ORM-heavy work in a thread instance, template_id, account_id = await sync_to_async(_do_create_sync)() await pubsub.publish("scope_created_from_template", instance.id) # Publish event profile = getattr(info.context.request, 'profile', None) await publish_scope_template_instantiated( scope_id=str(instance.id), template_id=template_id, account_id=account_id, triggered_by=profile ) return cast(ScopeType, instance) @strawberry.mutation(description="Create a ScopeTemplate (and nested Areas/Tasks) from a JSON payload") async def create_scope_template_from_json( self, payload: JSON, replace: bool = False, info: Info | None = None, ) -> ScopeTemplateType: """ Accepts a JSON object matching the builder payload shape. If replace=True and a template with the same name exists, it will be deleted first. """ def _do_create_sync(): if not isinstance(payload, dict): raise ValueError("payload must be a JSON object") name = payload.get("name") if not name or not isinstance(name, str): raise ValueError("payload.name is required and must be a string") if replace: ScopeTemplate.objects.filter(name=name).delete() elif ScopeTemplate.objects.filter(name=name).exists(): raise ValueError( f"A ScopeTemplate named '{name}' already exists (use replace=true to overwrite)" ) tpl = build_scope_template(payload) return tpl instance = await sync_to_async(_do_create_sync)() await pubsub.publish("scope_template_created", instance.id) # Publish event if info: profile = getattr(info.context.request, 'profile', None) await publish_scope_template_created( template_id=str(instance.id), triggered_by=profile ) return cast(ScopeTemplateType, instance)