Как определить правильный тип в GraphQL при использовании интерфейса и встроенных фрагментов - PullRequest
1 голос
/ 12 февраля 2020

Я столкнулся с проблемой, когда мне нужно сослаться на разрешенное поле на родительском элементе внутри __resolveType. К сожалению, поле, на которое мне нужно сослаться, пришло не как часть исходного ответа API для родителя, а из другого распознавателя поля, который я бы не имел значения, но на самом деле это так, поэтому он не определен.

Но мне нужны эти поля (в этом примере; obj.barCount и obj.bazCount), чтобы можно было выполнить следующий запрос, поэтому я зашел в тупик. Мне нужно, чтобы они были доступны в функции resolType, чтобы я мог использовать их, чтобы определить, какой тип разрешить в случае определения этого поля.

Вот пример:

Запрос graphql I wi sh, чтобы иметь возможность:

{
  somethings { 
    hello
    ... on HasBarCount {
      barCount
    }
    ... on HasBazCount {
      bazCount
    }
  }
}

Схема:

type ExampleWithBarCount implements Something & HasBarCount & Node {
  hello: String!
  barCount: Int
}

type ExampleWithBazCount implements Something & HasBazCount & Node {
  hello: String!
  bazCount: Int
}

interface Something {
  hello: String!
}

interface HasBarCount {
  barCount: Int
}

interface HasBazCount {
  bazCount: Int
}

Решатели:

ExampleWithBarCount: {
  barCount: (obj) => {
    return myApi.getBars(obj.id).length || 0
  }
}

ExampleWithBazCount {
  bazCount: (obj) => {
    return myApi.getBazs(obj.id).length || 0
  }
}

Задача:

Something: {
  __resolveType(obj) {
    console.log(obj.barCount) // Problem: this is always undefined
    console.log(obj.bazCount) // Problem: this is always undefined

    if (obj.barCount) {
      return 'ExampleWithBarCount';
    }

    if (obj.bazCount) {
      return 'ExampleWithBazCount';
    }

    return null;
  }
}

Любые идеи альтернативных решений или что я упускаю?

Вот еще немного о сценарии использования.

В базе данных у нас есть таблица «сущность». Эта таблица очень проста и только действительно важными столбцами являются id, parent_id, name. type, а затем, конечно, вы можете прикрепить к нему некоторые дополнительные метаданные.

Как и в случае с «entity», типы создаются динамически из внутренней системы управления, и после этого вы можете назначить тип вашей конкретной сущности.

Основная цель «сущности» - установить sh иерархию / дерево вложенных сущностей по parent_id и с разными «типами» (в столбце типа сущности). Будут разные метаданные, но давайте не будем заострять на этом внимание.

Примечание: сущность может быть названа как угодно, а тип может быть любым.

В API у нас будет конечная точка где мы можем получить все сущности с указанным c типом (sidenote: и в дополнение к единственному типу на праве, у нас также есть конечная точка для получения всех сущностей по их таксономии / термину).

В В первой реализации я смоделировал схему, добавив все «известные» типы, которые были в моей спецификации, из UX'er во время разработки. Дерево сущностей может иметь вид, например:

  • Компания (или Организация, ..., Корпорация ... и т. Д. 1079 *)
    • Филиал (или Регион, ... , et c)
    • Фабрика (или Здание, объект, ..., et c)
      • Зона (или Комната, ..., et c)

Но эта иерархия - только один из способов, которым это можно сделать. Имена каждого из них могут быть совершенно разными, и вы можете переместить некоторые из них на уровень вверх или вниз или не иметь их вообще, в зависимости от варианта использования.

Единственное, что установлено в камне, это то, что они совместно использовать одну и ту же таблицу базы данных, будут определены столбец / поле типа, и они могут иметь или не иметь дочерние элементы. Нижний слой в иерархии не будет иметь дочерних элементов, а будет иметь машины. Остальные только разные метаданные, которые, я думаю, мы должны игнорировать, чтобы не усложнять это.

Как видите, иерархия должна быть очень гибкой и динамичной c, поэтому я понял, что это не так отличное решение, которое я начал.

На самом нижнем уровне «Зона» в этом случае должно быть поле «машины», которое должно возвращать список машин (они находятся в «машинах»). "таблица в БД, и не часть иерархии, а просто связанная с" entity_id "в таблице" machines ".

У меня были типы схем и преобразователи для всех в приведенной выше иерархии: Организация, Филиал , Factory, Zone et c, но я по большей части просто повторялся, поэтому я подумал, что могу обратиться к интерфейсам, чтобы попытаться обобщить это подробнее.

Поэтому вместо того, чтобы делать

{
  companies{
    name
    branchCount
    buildingCount
    zoneCount
    branches {
      name
      buildingCount
      zoneCount
      buildings {
        name
        zoneCount
        zones {
          name
          machines {
            name
          } 
        }
      }
    }
  }
}

И, добавив схему / преобразователи для всех имен имен сущностей, я подумал, что это будет работать:

{
  entities(type: "companies") { 
    name
    ... on HasEntityCount {
      branchCount: entityCount(type: "branch")
      buildingCount: entityCount(type: "building")
      zoneCount: entityCount(type: "zone")
    }
    ... on HasSubEntities {
      entities(type: "branch") {
        name
        ... on HasEntityCount {
          buildingCount: entityCount(type: "building")
          zoneCount: entityCount(type: "zone")
        }
        ... on HasMachineCount {
          machineCount
        }
        ... on HasSubEntities {
          entities(type: "building") {
            name
            ... on HasEntityCount {
              zoneCount: entityCount(type: "zone")
            }
            ... on HasMachineCount {
              machineCount
            }
            ... on HasSubEntities {
              entities(type: "zone") {
                name
                ... on HasMachines {
                  machines
                }
              }
            }
          }
        }
      }
    }
  }
}

С интерфейсами:

interface HasMachineCount {
  machineCount: Int
}

interface HasEntityCount  {
  entitiyCount(type: String): Int
}

interface HasSubEntities {
  entities(
    type: String
  ): [Entity!]
}

interface HasMachines {
  machines: [Machine!]
}

interface Entity {
  id: ID!
  name: String!
  type: String!
}

ниже работы ks, но я действительно хочу избежать одного типа с большим количеством необязательных / пустых полей:

type Entity {
  id: ID!
  name: String!
  type: String!
  # Below is what I want to avoid, by using interfaces
  # Imagine how this would grow
  entityCount
  machineCount
  entities
  machines
}

В моей собственной логике c Мне все равно, как называются сущности, только какие поля ожидаются. Я бы хотел избежать одного типа Entity с множеством полей, допускающих обнуляемость, поэтому я подумал, что интерфейсы или объединения будут полезны для разделения вещей, поэтому я в итоге получил HasSubEntities, HasEntityCount, HasMachineCount и HasMachines, поскольку нижний объект не будет иметь сущности ниже, и только нижняя сущность будет иметь машины. Но в реальном коде их было бы намного больше, чем 2, и он мог бы закончиться множеством необязательных полей, если, я думаю, не использовать интерфейсы или объединения каким-либо образом.

1 Ответ

1 голос
/ 12 февраля 2020

Здесь есть две отдельные проблемы.

Во-первых, GraphQL разрешает поля сверху вниз. Родительские поля всегда разрешаются перед любыми дочерними полями. Таким образом, невозможно получить доступ к значению, к которому разрешено поле, из распознавателя родительского поля (или распознавателя поля «родственный»). В случае полей с абстрактным типом это относится и к распознавателям type . Тип поля будет разрешен до того, как будут вызваны любые дочерние распознаватели. только способ обойти эту проблему - переместить соответствующие логи c из дочернего распознавателя внутрь родительского распознавателя.

Two, предполагая, что поле somethings имеет тип Something (или [Something], et c.) Запрос, который вы пытаетесь выполнить, никогда не будет работать, потому что HasBarCount и HasBazCount не являются подтипами Something. Когда вы говорите GraphQL, что поле имеет абстрактный тип (интерфейс или объединение), вы говорите, что то, что возвращается полем, может быть одним из нескольких типов объектов, которые будут сужены до ровно одного типа объекта во время выполнения. Возможные типы - это либо типы, которые составляют объединение, либо типы, которые реализуют интерфейс.

Объединение может состоять только из типов объектов, а не интерфейсов или других объединений. Аналогично, только тип объекта может реализовывать интерфейс - другие интерфейсы или объединения могут не реализовывать интерфейсы. Поэтому при использовании встроенных фрагментов с полем, которое возвращает абстрактный тип, условие on для этих встроенных фрагментов всегда будет типом объекта и должно быть одним из возможных типов для рассматриваемого абстрактного типа.

Поскольку это псевдокод, не совсем понятно, какие бизнес-правила или прецедент вы пытаетесь смоделировать с помощью такой схемы. Но я могу сказать, что в общем случае нет необходимости создавать интерфейс и иметь тип, реализующий его , если только вы не планируете добавить поле в своей схеме, в котором этот интерфейс будет иметь этот тип.

Редактировать: на высоком уровне это звучит так, как будто вы, вероятно, просто хотите сделать что-то вроде этого:

type Query {
  entities(type: String!): [Entity!]!
}

interface Entity {
  type: String!
  # other shared entity fields
}

type EntityWithChildren implements Entity {
  type: String!
  children: [Entity!]!
}

type EntityWithModels implements Entity {
  type: String!
  models: [Model!]!
}

Преобразователь типов должен проверить, есть ли у нас модели, поэтому вы захотите сделать убедитесь, что вы извлекаете связанные модели при извлечении сущности (в отличие от извлечения их внутри решателя models). Кроме того, вы можете добавить в свою базу данных какой-то столбец, который определяет сущность как «низшую» в иерархии, и в этом случае вы можете просто использовать это свойство вместо этого.

function resolveType (obj) {
  return obj.models ? 'EntityWithModels' : 'EntityWithChildren'
}

Теперь ваш запрос выглядит следующим образом:

entities {
  type
  ... on EntityWithModels {
    models { ... }
  }
  ... on EntityWithChildren {
    children {
      ... on EntityWithModels {
        models { ... }
      }
      ... on EntityWithChildren {
        # etc.
      }
    }
  }
}

Подсчет немного сложнее из-за изменчивости в именах сущностей и изменчивости в глубине иерархии. Я бы предложил просто позволить клиенту вычислить счетчик, как только он получит весь граф с сервера. Если вы действительно хотите добавить поля подсчета, вам нужно иметь такие поля, как childrenCount, grandchildrenCount, et c. Тогда единственный способ правильно заполнить эти поля - извлечь весь граф в root.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...