Skip to content

Commit dff01bf

Browse files
committed
Merge branch 'feat_65186_itsystem_roles' into 'master'
Feat 65186 itsystem roles See merge request rammearkitektur/os2mo!2619
2 parents c716863 + 9d0c7b3 commit dff01bf

File tree

2 files changed

+350
-23
lines changed

2 files changed

+350
-23
lines changed

backend/mora/graphapi/versions/latest/schema.py

Lines changed: 95 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import json
66
import re
77
from base64 import b64encode
8+
from collections.abc import Awaitable
89
from collections.abc import Callable
910
from datetime import date
1011
from datetime import datetime
@@ -65,6 +66,7 @@
6566

6667
from ...version import Version as GraphQLVersion
6768
from .filters import EmployeeFilter
69+
from .filters import ITSystemFilter
6870
from .filters import ITUserFilter
6971
from .filters import ManagerFilter
7072
from .filters import OrganisationUnitFilter
@@ -166,29 +168,6 @@ async def wrapper(*args: Any, **kwargs: Any) -> R | None:
166168
return wrapper
167169

168170

169-
def result_translation(mapper: Callable) -> Callable:
170-
def wrapper(resolver_func: Callable) -> Callable:
171-
@wraps(resolver_func)
172-
async def mapped_resolver(*args: Any, **kwargs: Any) -> Any:
173-
result = await resolver_func(*args, **kwargs)
174-
return mapper(result)
175-
176-
return mapped_resolver
177-
178-
return wrapper
179-
180-
181-
to_list = result_translation(
182-
lambda result: list(chain.from_iterable(result.values())),
183-
)
184-
to_only = result_translation(
185-
lambda result: only(chain.from_iterable(result.values())),
186-
)
187-
to_one = result_translation(
188-
lambda result: one(chain.from_iterable(result.values())),
189-
)
190-
191-
192171
def uuid2list(uuid: UUID | None) -> list[UUID]:
193172
"""Convert an optional uuid to a list.
194173
@@ -404,6 +383,66 @@ async def validities(
404383
)
405384

406385

386+
ResolverResult = dict[UUID, list[MOObject]]
387+
ResolverFunction = Callable[..., Awaitable[ResolverResult]]
388+
389+
390+
def result_translation(
391+
mapper: Callable[[ResolverResult], R],
392+
) -> Callable[[ResolverFunction], Callable[..., Awaitable[R]]]:
393+
def wrapper(
394+
resolver_func: ResolverFunction,
395+
) -> Callable[..., Awaitable[R]]:
396+
@wraps(resolver_func)
397+
async def mapped_resolver(*args: Any, **kwargs: Any) -> Any:
398+
result = await resolver_func(*args, **kwargs)
399+
return mapper(result)
400+
401+
return mapped_resolver
402+
403+
return wrapper
404+
405+
406+
def to_response_list(
407+
model: type[MOObject],
408+
) -> Callable[[ResolverFunction], Callable[..., Awaitable[list[Response[MOObject]]]]]:
409+
def result2response_list(result: ResolverResult) -> list[Response[MOObject]]:
410+
# The type checker really does not like the below code.
411+
#
412+
# Mypy says: 'error: Variable "model" is not valid as a type', however every
413+
# attept to appease mypy by fixing the typing has ended up making the code
414+
# non-functional on runtime.
415+
#
416+
# Additionally it complains about construction of the Response object being
417+
# illegal as it 'Expected no arguments to "Response" constructor', however
418+
# attempting to resolve this using Pydantic's 'parse_obj_as' results in an
419+
# 'Fields of type \"<class 'Response'>\" are not supported."' error from
420+
# strawberry on runtime, whether implemented as:
421+
# '[parse_obj_as(T, x) for x in xs]' or 'parse_obj_as(list[T], xs)'.
422+
#
423+
# If you try to fix this typing issues here, please increment the following
424+
# counter as a warning to the next guy:
425+
#
426+
# total_hours_wasted_here = 4
427+
return [
428+
Response[model](uuid=uuid, object_cache=objects) # type: ignore
429+
for uuid, objects in result.items()
430+
]
431+
432+
return result_translation(result2response_list)
433+
434+
435+
to_list = result_translation(
436+
lambda result: list(chain.from_iterable(result.values())),
437+
)
438+
to_only = result_translation(
439+
lambda result: only(chain.from_iterable(result.values())),
440+
)
441+
to_one = result_translation(
442+
lambda result: one(chain.from_iterable(result.values())),
443+
)
444+
445+
407446
def response2model(response: Response[MOObject]) -> MOObject:
408447
if not hasattr(response, "__orig_class__"): # pragma: no cover
409448
raise ValueError(
@@ -2569,6 +2608,39 @@ async def name(self, root: ITSystemRead) -> str:
25692608
async def user_key(self, root: ITSystemRead) -> str:
25702609
return root.user_key
25712610

2611+
roles: list[Response[LazyClass]] = strawberry.field(
2612+
resolver=to_response_list(LazyClass)(
2613+
seed_resolver(
2614+
class_resolver,
2615+
{
2616+
"it_system": lambda root: ITSystemFilter(
2617+
uuids=uuid2list(root.uuid),
2618+
# The following two arguments are not strictly necessary because
2619+
# we are filtering by UUIDs which handles dates differently than
2620+
# normal filters.
2621+
# If we were to instead filter by say 'user_keys' the arguments
2622+
# would be necessary. They are added here anyway to simplify the
2623+
# migration in the future when our filtering achieve consistent
2624+
# behavior across all filters.
2625+
from_date=None,
2626+
to_date=None,
2627+
)
2628+
},
2629+
)
2630+
),
2631+
description=dedent(
2632+
"""\
2633+
Rolebinding roles related to the IT-system.
2634+
2635+
Examples of user-keys:
2636+
* `"AD Read"`
2637+
* `"AD Write"`
2638+
* `"SAP Admin"`
2639+
"""
2640+
),
2641+
permission_classes=[IsAuthenticatedPermission, gen_read_permission("class")],
2642+
)
2643+
25722644
# TODO: Document this
25732645
system_type: str | None = strawberry.auto
25742646

0 commit comments

Comments
 (0)