from typing import cast import strawberry from strawberry.types import Info from asgiref.sync import sync_to_async from core.graphql.inputs.project_scope import ( ProjectScopeInput, ProjectScopeUpdateInput, ProjectScopeCategoryInput, ProjectScopeCategoryUpdateInput, ProjectScopeTaskInput, ProjectScopeTaskUpdateInput, CreateProjectScopeFromTemplateInput, ) from core.graphql.types.project_scope import ( ProjectScopeType, ProjectScopeCategoryType, ProjectScopeTaskType, ) from core.graphql.utils import create_object, update_object, delete_object from core.models.account import Account, AccountAddress from core.models.project import Project from core.models.project_scope import ProjectScope, ProjectScopeCategory, ProjectScopeTask from core.models.project_scope_template import ProjectScopeTemplate from core.services.events import ( publish_project_scope_created, publish_project_scope_updated, publish_project_scope_deleted, publish_project_scope_category_created, publish_project_scope_category_updated, publish_project_scope_category_deleted, publish_project_scope_task_created, publish_project_scope_task_updated, publish_project_scope_task_deleted, publish_project_scope_template_instantiated, ) @strawberry.type class Mutation: # ProjectScope CRUD @strawberry.mutation(description="Create a new ProjectScope") async def create_project_scope(self, input: ProjectScopeInput, info: Info) -> ProjectScopeType: instance = await create_object(input, ProjectScope) # Publish event profile = getattr(info.context.request, 'profile', None) await publish_project_scope_created( scope_id=str(instance.id), project_id=str(instance.project_id), triggered_by=profile ) return cast(ProjectScopeType, instance) @strawberry.mutation(description="Update an existing ProjectScope") async def update_project_scope(self, input: ProjectScopeUpdateInput, info: Info) -> ProjectScopeType: instance = await update_object(input, ProjectScope) # Publish event profile = getattr(info.context.request, 'profile', None) await publish_project_scope_updated( scope_id=str(instance.id), project_id=str(instance.project_id), triggered_by=profile ) return cast(ProjectScopeType, instance) @strawberry.mutation(description="Delete a ProjectScope") async def delete_project_scope(self, id: strawberry.ID, info: Info) -> strawberry.ID: instance = await delete_object(id, ProjectScope) if not instance: raise ValueError(f"ProjectScope with ID {id} does not exist") # Publish event profile = getattr(info.context.request, 'profile', None) await publish_project_scope_deleted( scope_id=str(id), project_id=str(instance.project_id), triggered_by=profile ) return id @strawberry.mutation(description="Create a ProjectScopeCategory") async def create_project_scope_category(self, input: ProjectScopeCategoryInput, info: Info) -> ProjectScopeCategoryType: instance = await create_object(input, ProjectScopeCategory) # Publish event profile = getattr(info.context.request, 'profile', None) await publish_project_scope_category_created( category_id=str(instance.id), scope_id=str(instance.scope_id), triggered_by=profile ) return cast(ProjectScopeCategoryType, instance) @strawberry.mutation(description="Update a ProjectScopeCategory") async def update_project_scope_category(self, input: ProjectScopeCategoryUpdateInput, info: Info) -> ProjectScopeCategoryType: instance = await update_object(input, ProjectScopeCategory) # Publish event profile = getattr(info.context.request, 'profile', None) await publish_project_scope_category_updated( category_id=str(instance.id), scope_id=str(instance.scope_id), triggered_by=profile ) return cast(ProjectScopeCategoryType, instance) @strawberry.mutation(description="Delete a ProjectScopeCategory") async def delete_project_scope_category(self, id: strawberry.ID, info: Info) -> strawberry.ID: instance = await delete_object(id, ProjectScopeCategory) if not instance: raise ValueError(f"ProjectScopeCategory with ID {id} does not exist") # Publish event profile = getattr(info.context.request, 'profile', None) await publish_project_scope_category_deleted( category_id=str(id), scope_id=str(instance.scope_id), triggered_by=profile ) return id @strawberry.mutation(description="Create a ProjectScopeTask") async def create_project_scope_task(self, input: ProjectScopeTaskInput, info: Info) -> ProjectScopeTaskType: instance = await create_object(input, ProjectScopeTask) # Publish event profile = getattr(info.context.request, 'profile', None) await publish_project_scope_task_created( task_id=str(instance.id), category_id=str(instance.category_id), triggered_by=profile ) return cast(ProjectScopeTaskType, instance) @strawberry.mutation(description="Update a ProjectScopeTask") async def update_project_scope_task(self, input: ProjectScopeTaskUpdateInput, info: Info) -> ProjectScopeTaskType: instance = await update_object(input, ProjectScopeTask) # Publish event profile = getattr(info.context.request, 'profile', None) await publish_project_scope_task_updated( task_id=str(instance.id), category_id=str(instance.category_id), triggered_by=profile ) return cast(ProjectScopeTaskType, instance) @strawberry.mutation(description="Delete a ProjectScopeTask") async def delete_project_scope_task(self, id: strawberry.ID, info: Info) -> strawberry.ID: instance = await delete_object(id, ProjectScopeTask) if not instance: raise ValueError(f"ProjectScopeTask with ID {id} does not exist") # Publish event profile = getattr(info.context.request, 'profile', None) await publish_project_scope_task_deleted( task_id=str(id), category_id=str(instance.category_id), triggered_by=profile ) return id @strawberry.mutation(description="Instantiate a ProjectScope (with Categories and Tasks) from a ProjectScopeTemplate") async def create_project_scope_from_template(self, input: CreateProjectScopeFromTemplateInput, info: Info) -> ProjectScopeType: def _do_create_sync() -> tuple[ProjectScope, str, str]: # Load required objects synchronously (ORM-safe in this thread) project = ( Project.objects .select_related("account_address__account") .get(pk=input.project_id.node_id) ) tpl = ProjectScopeTemplate.objects.get(pk=input.template_id.node_id) # Defaults derived from project (if project has an account_address) account = None account_address = None if project.account_address_id: account_address = project.account_address account = account_address.account if input.account_address_id: account_address = AccountAddress.objects.get(pk=input.account_address_id.node_id) account = account_address.account if input.account_id: account = Account.objects.get(pk=input.account_id.node_id) # Instantiate the ProjectScope object from the template instance = tpl.instantiate( project=project, 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, ) # Persist the relation on the project project.scope = instance project.save(update_fields=["scope"]) return instance, str(tpl.id), str(project.id) instance, template_id, project_id = await sync_to_async(_do_create_sync, thread_sensitive=True)() # Publish event profile = getattr(info.context.request, 'profile', None) await publish_project_scope_template_instantiated( scope_id=str(instance.id), template_id=template_id, project_id=project_id, triggered_by=profile ) return cast(ProjectScopeType, instance)