В комментариях к первому вопросу было несколько предложений. Я вернусь к некоторым из них в конце этого предложения.
Я думал об этой проблеме, а также о том, что она кажется постоянным вопросом среди разработчиков. Я пришел к выводу, что мы можем что-то упустить в том смысле, в каком мы хотим редактировать наш график, а именно операции с краями. Я думаю, что мы пытаемся делать ребра с помощью узловых операций. Чтобы проиллюстрировать это, создание графика на таком языке, как точка (Graphviz), может выглядеть следующим образом:
digraph D {
/* Nodes */
A
B
C
/* Edges */
A -> B
A -> C
A -> D
}
Следуя этой схеме, возможно, мутация graphql в вопросе должна выглядеть следующим образом:
mutation {
# Nodes
n1: createUser(username: "new user", password: "secret"){
uid
username
}
n2: updateGroup(gid: "group id"){
gid
name
}
# Edges
addUserToGroup(user: "n1", group: "n2"){
status
}
}
Входные данные «операции края» addUserToGroup
будут псевдонимами предыдущих узлов в запросе на мутацию.
Это также позволит декорировать операции края с проверками разрешений ( разрешения на создание отношения могут отличаться от прав доступа для каждого объекта).
Мы определенно можем разрешить такой запрос уже. Что менее уверенно, так это то, что базовые структуры, в частности Graphene python, предоставляют механизмы, позволяющие реализовать addUserToGroup
(наличие предыдущей мутации приводит к контексту разрешения). Я думаю о введении dict
предыдущих результатов в контексте графена. В случае успеха я постараюсь дополнить ответ техническими подробностями.
Может быть, уже есть способ достичь чего-то подобного, я также поищу это и дополню ответ, если найден.
Если выяснится, что описанная выше схема невозможна или найдена плохая практика, я думаю, что я буду придерживаться двух отдельных мутаций.
РЕДАКТИРОВАТЬ 1: обмен результатами
Я проверил способ разрешения запроса, как описано выше, используя Graphene- python middleware и базовый класс мутаций для совместного использования результатов. Я создал однофайловую python программу, доступную на Github , чтобы проверить это. Или поиграйте с ним в Repl .
Промежуточное ПО довольно простое и добавляет в резольверы параметр dict как kwarg
:
class ShareResultMiddleware:
shared_results = {}
def resolve(self, next, root, info, **args):
return next(root, info, shared_results=self.shared_results, **args)
Базовый класс также довольно прост и управляет вставкой результатов в словарь:
class SharedResultMutation(graphene.Mutation):
@classmethod
def mutate(cls, root: None, info: graphene.ResolveInfo, shared_results: dict, *args, **kwargs):
result = cls.mutate_and_share_result(root, info, *args, **kwargs)
if root is None:
node = info.path[0]
shared_results[node] = result
return result
@staticmethod
def mutate_and_share_result(*_, **__):
return SharedResultMutation() # override
Мутация, подобная узлу, которая должна соответствовать шаблону общего результата, будет наследоваться от SharedResultMutation
вместо Mutation
и переопределять mutate_and_share_result
вместо mutate
:
class UpsertParent(SharedResultMutation, ParentType):
class Arguments:
data = ParentInput()
@staticmethod
def mutate_and_share_result(root: None, info: graphene.ResolveInfo, data: ParentInput, *___, **____):
return UpsertParent(id=1, name="test") # <-- example
Мутации, подобные ребрам, должны получить доступ к диктату shared_results
, поэтому они переопределяют mutate
напрямую:
class AddSibling(SharedResultMutation):
class Arguments:
node1 = graphene.String(required=True)
node2 = graphene.String(required=True)
ok = graphene.Boolean()
@staticmethod
def mutate(root: None, info: graphene.ResolveInfo, shared_results: dict, node1: str, node2: str): # ISSUE: this breaks type awareness
node1_ : ChildType = shared_results.get(node1)
node2_ : ChildType = shared_results.get(node2)
# do stuff
return AddSibling(ok=True)
И вот в принципе и все (остальное - это обычные графеновые шаблоны и тестовые макеты). Теперь мы можем выполнить запрос, подобный следующему:
mutation ($parent: ParentInput, $child1: ChildInput, $child2: ChildInput) {
n1: upsertParent(data: $parent) {
pk
name
}
n2: upsertChild(data: $child1) {
pk
name
}
n3: upsertChild(data: $child2) {
pk
name
}
e1: setParent(parent: "n1", child: "n2") { ok }
e2: setParent(parent: "n1", child: "n3") { ok }
e3: addSibling(node1: "n2", node2: "n3") { ok }
}
Проблема заключается в том, что аргументы в виде реберной мутации не удовлетворяют осведомленности о типе 1060 *, поддерживаемой GraphQL: в духе GraphQL, node1
и node2
должны быть набраны graphene.Field(ChildType)
вместо graphene.String()
, как в этой реализации. РЕДАКТИРОВАТЬ Добавлена базовая проверка c типов для узлов ввода, подобных краевым мутациям .
РЕДАКТИРОВАТЬ 2: вложенные создания
Для сравнения я также реализовал шаблон вложенности, в котором разрешаются только создания (это единственный случай, когда у нас не может быть данных в предыдущем запросе), однофайловая программа, доступная на Github .
Это класс c Графен, кроме мутации UpsertChild
, где мы добавили поле для решения вложенных созданий и их резольверы:
class UpsertChild(graphene.Mutation, ChildType):
class Arguments:
data = ChildInput()
create_parent = graphene.Field(ParentType, data=graphene.Argument(ParentInput))
create_sibling = graphene.Field(ParentType, data=graphene.Argument(lambda: ChildInput))
@staticmethod
def mutate(_: None, __: graphene.ResolveInfo, data: ChildInput):
return Child(
pk=data.pk
,name=data.name
,parent=FakeParentDB.get(data.parent)
,siblings=[FakeChildDB[pk] for pk in data.siblings or []]
) # <-- example
@staticmethod
def resolve_create_parent(child: Child, __: graphene.ResolveInfo, data: ParentInput):
parent = UpsertParent.mutate(None, __, data)
child.parent = parent.pk
return parent
@staticmethod
def resolve_create_sibling(node1: Child, __: graphene.ResolveInfo, data: 'ChildInput'):
node2 = UpsertChild.mutate(None, __, data)
node1.siblings.append(node2.pk)
node2.siblings.append(node1.pk)
return node2
Итак, количество дополнительные вещи малы по сравнению с узлом узел + ребро. Теперь мы можем выполнить запрос вроде:
mutation ($parent: ParentInput, $child1: ChildInput, $child2: ChildInput) {
n1: upsertChild(data: $child1) {
pk
name
siblings { pk name }
parent: createParent(data: $parent) { pk name }
newSibling: createSibling(data: $child2) { pk name }
}
}
Однако мы можем видеть, что в отличие от того, что было возможно с шаблоном node + edge, (shared_result_mutation.py) мы не можем установить родителя нового брат в той же мутации. Очевидная причина в том, что у нас нет его данных (в частности, его pk). Другая причина в том, что порядок для вложенных мутаций не гарантирован. Поэтому не может создать, например, мутацию без данных assignParentToSiblings
, которая установила бы родителя всех братьев и сестер текущего root дочернего элемента, поскольку вложенный брат может быть создан перед вложенным родителем.
В некоторых практических случаях нам просто нужно создать новый объект и затем связать его с существующим объектом. Вложенность может удовлетворить эти варианты использования.
В комментариях к вопросу предлагалось использовать вложенные данные для мутаций. На самом деле это была моя первая реализация этой функции, и я отказался от нее из-за соображений безопасности. Проверки разрешений используют декораторы и выглядят так (у меня нет мутаций в Книге):
class UpsertBook(common.mutations.MutationMixin, graphene.Mutation, types.Book):
class Arguments:
data = types.BookInput()
@staticmethod
@authorize.grant(authorize.admin, authorize.owner, model=models.Book)
def mutate(_, info: ResolveInfo, data: types.BookInput) -> 'UpsertBook':
return UpsertBook(**data) # <-- example
Не думаю, что мне следует также выполнять эту проверку в другом месте, внутри другой мутации с вложенными данными для пример. Кроме того, вызов этого метода в другой мутации потребует импорта между модулями мутации, что я не считаю хорошей идеей. Я действительно думал, что решение должно опираться на возможности разрешения GraphQL, поэтому я посмотрел на вложенные мутации, что заставило меня сначала задать вопрос об этом посте.
Кроме того, я сделал больше тестов uuid Идея из вопроса (с юнит-тестом Tescase). Оказывается, что быстрые последовательные вызовы python uuid.uuid4 могут конфликтовать, поэтому эта опция мне не нужна.