-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #249 from ynput/248-graphql-kanban-resolver
Server-side kanban resolver
- Loading branch information
Showing
6 changed files
with
274 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
from datetime import datetime | ||
from typing import Any | ||
|
||
import strawberry | ||
|
||
|
||
@strawberry.type | ||
class KanbanNode: | ||
project_name: str = strawberry.field() | ||
project_code: str = strawberry.field() | ||
id: str = strawberry.field() | ||
name: str = strawberry.field() | ||
label: str | None = strawberry.field() | ||
status: str = strawberry.field() | ||
tags: list[str] = strawberry.field() | ||
task_type: str = strawberry.field() | ||
assignees: list[str] = strawberry.field() | ||
updated_at: datetime = strawberry.field() | ||
created_at: datetime = strawberry.field() | ||
due_date: datetime | None = strawberry.field(default=None) | ||
folder_id: str = strawberry.field() | ||
folder_name: str = strawberry.field() | ||
folder_label: str | None = strawberry.field() | ||
folder_path: str = strawberry.field() | ||
thumbnail_id: str | None = strawberry.field(default=None) | ||
last_version_with_thumbnail_id: str | None = strawberry.field(default=None) | ||
|
||
|
||
def kanban_node_from_record( | ||
project_name: str | None, | ||
record: dict[str, Any], | ||
context: dict[str, Any], | ||
) -> KanbanNode: | ||
record = dict(record) | ||
record.pop("cursor", None) | ||
|
||
project_name = record.pop("project_name", project_name) | ||
assert project_name, "project_name is required" | ||
|
||
due_date = record.pop("due_date", None) | ||
if isinstance(due_date, datetime): | ||
due_date = due_date.replace(tzinfo=None) | ||
elif isinstance(due_date, str): | ||
due_date = datetime.fromisoformat(due_date) | ||
record["due_date"] = due_date | ||
|
||
return KanbanNode( | ||
project_name=project_name, | ||
**record, | ||
) | ||
|
||
|
||
KanbanNode.from_record = staticmethod(kanban_node_from_record) # type: ignore |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
# import time | ||
|
||
from ayon_server.entities import UserEntity | ||
from ayon_server.graphql.connections import KanbanConnection | ||
from ayon_server.graphql.edges import KanbanEdge | ||
from ayon_server.graphql.nodes.kanban import KanbanNode | ||
from ayon_server.graphql.resolvers.common import ( | ||
ARGBefore, | ||
ARGLast, | ||
resolve, | ||
) | ||
from ayon_server.graphql.types import Info | ||
from ayon_server.lib.postgres import Postgres | ||
from ayon_server.types import validate_name_list | ||
from ayon_server.utils import SQLTool | ||
|
||
|
||
def user_has_access(user: UserEntity, project_name: str) -> bool: | ||
if user.is_manager: | ||
return True | ||
return project_name in user.data.get("accessGroups", {}) | ||
|
||
|
||
async def get_kanban( | ||
root, | ||
info: Info, | ||
last: ARGLast = 2000, | ||
before: ARGBefore = None, | ||
projects: list[str] | None = None, | ||
assignees_any: list[str] | None = None, | ||
task_ids: list[str] | None = None, | ||
) -> KanbanConnection: | ||
""" | ||
Fetches tasks for the Kanban board. | ||
Parameters | ||
---------- | ||
last : ARGLast, optional | ||
The number of tasks to return, by default 2000. | ||
before : ARGBefore, optional | ||
The cursor to fetch tasks before, by default None. | ||
projects : list[str], optional | ||
List of project IDs to filter tasks. | ||
If not specified, tasks from all projects are listed. | ||
For non-managers, the result is limited to projects the user has access to. | ||
Inactive projects are never included. | ||
assignees_any : list[str], optional | ||
List of user names to filter tasks. | ||
If the invoking user is a manager, tasks assigned | ||
to the specified users are listed. | ||
If not provided, all tasks are listed regardless of assignees. | ||
For non-managers, this is always set to [user.name]. | ||
task_ids : list[str], optional | ||
If set, return explicit tasks by their IDs. | ||
This is used for fetching updates when a entity.task.* event is received. | ||
Returns | ||
------- | ||
KanbanConnection | ||
A connection object containing the fetched tasks. | ||
""" | ||
user = info.context["user"] | ||
|
||
project_data: list[dict[str, str]] = [] | ||
|
||
if not projects: | ||
q = "SELECT name, code FROM projects WHERE active IS TRUE" | ||
else: | ||
validate_name_list(projects) | ||
q = f""" | ||
SELECT name, code | ||
FROM projects | ||
WHERE name = ANY({SQLTool.array(projects, curly=True)}) | ||
""" | ||
async for row in Postgres.iterate(q): | ||
project_data.append(row) | ||
|
||
if not user.is_manager: | ||
assignees_any = [user.name] | ||
project_data = [p for p in project_data if user_has_access(user, p["name"])] | ||
elif assignees_any: | ||
validate_name_list(assignees_any) | ||
|
||
# Sub-query conditions | ||
|
||
sub_query_conds = [] | ||
|
||
if task_ids: | ||
# id_array sanitizes the input | ||
c = f"t.id IN {SQLTool.id_array(task_ids)}" | ||
sub_query_conds.append(c) | ||
|
||
if assignees_any: | ||
# assignees list is already sanitized at this point | ||
c = f"t.assignees && {SQLTool.array(assignees_any, curly=True)}" | ||
sub_query_conds.append(c) | ||
|
||
union_queries = [] | ||
for pdata in project_data: | ||
project_name = pdata["name"] | ||
project_code = pdata["code"] | ||
project_schema = f"project_{project_name}" | ||
uq = f""" | ||
SELECT | ||
'{project_name}' AS project_name, | ||
'{project_code}' AS project_code, | ||
t.id as id, | ||
t.name as name, | ||
t.label as label, | ||
t.status as status, | ||
t.tags as tags, | ||
t.task_type as task_type, | ||
t.assignees as assignees, | ||
t.updated_at as updated_at, | ||
t.created_at as created_at, | ||
t.attrib->>'endDate' as due_date, | ||
f.id as folder_id, | ||
f.name as folder_name, | ||
f.label as folder_label, | ||
h.path as folder_path, | ||
t.thumbnail_id as thumbnail_id, | ||
vs.version_id as last_version_with_thumbnail_id | ||
FROM {project_schema}.tasks t | ||
JOIN {project_schema}.folders f ON f.id = t.folder_id | ||
JOIN {project_schema}.hierarchy h ON h.id = f.id | ||
LEFT JOIN LATERAL ( | ||
SELECT | ||
v.id AS version_id | ||
FROM | ||
{project_schema}.versions v | ||
WHERE | ||
v.task_id = t.id | ||
AND v.thumbnail_id IS NOT NULL | ||
ORDER BY | ||
CASE | ||
WHEN v.version < 0 THEN 1 | ||
ELSE 0 | ||
END DESC, | ||
v.version DESC | ||
LIMIT 1 | ||
) vs ON true | ||
{SQLTool.conditions(sub_query_conds)} | ||
""" | ||
union_queries.append(uq) | ||
|
||
unions = " UNION ALL ".join(union_queries) | ||
|
||
cursor = "updated_at" | ||
|
||
query = f""" | ||
SELECT | ||
{cursor} as cursor, | ||
* FROM ({unions}) dummy | ||
ORDER BY | ||
due_date DESC NULLS LAST, | ||
updated_at DESC | ||
""" | ||
|
||
# | ||
# Execute the query | ||
# | ||
|
||
res = await resolve( | ||
KanbanConnection, | ||
KanbanEdge, | ||
KanbanNode, | ||
None, | ||
query, | ||
None, | ||
last, | ||
context=info.context, | ||
) | ||
return res |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters