Как изменить родительский узел до того, как он будет передан распознавателю?

Предположим, у нас есть эта схема GraphQL:

type Venue implements Node {
  country: Country!
  id: ID!
  name: String!
  nid: String!
  url: String!
}

который поддерживается этим преобразователем:

// @flow

import type {
  VenueRecordType,
  ResolverType,
} from '../types';

const Venue: ResolverType<VenueRecordType> = {
  country: (node, parameters, context) => {
    return context.loaders.CountryByIdLoader.load(node.countryId);
  },
};

export default Venue;

Я хочу иметь возможность изменять значение параметра parent/node до того, как оно будет использоваться полями преобразователя.

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

// @flow

import type {
  VenueRecordType,
  ResolverType,
} from '../types';

const createNodeDecorator = (fieldResolver) => {
  const updateNode = (node) => {
    // https://media0.giphy.com/media/12NUbkX6p4xOO4/giphy.gif
    return node;
  };

  return (parent, parameteres, context, info) => {
    return fieldResolver(updateNode(parent), parameteres, context, info);
  };
};

const Venue: ResolverType<VenueRecordType> = {
  country: createNodeDecorator((node, parameters, context) => {
    return context.loaders.CountryByIdLoader.load(node.countryId);
  }),
  id: createNodeDecorator((node) => {
    return node.id;
  }),
  name: createNodeDecorator((node) => {
    return node.name;
  }),
  nid: createNodeDecorator((node) => {
    return node.nid;
  }),
  url: createNodeDecorator((node) => {
    return node.url;
  }),
};

export default Venue;

Есть ли способ лучше?

В идеале у меня был бы хук __load, который вызывается перед использованием распознавателя, например.

const Venue: ResolverType<VenueRecordType> = {
  __load: (parent, parameteres, context, info, next) => {
    next(parent, parameteres, context, info);
  },
  country: (node, parameters, context) => {
    return context.loaders.CountryByIdLoader.load(node.countryId);
  },
};

Но этого (насколько я могу судить) не существует.

Как изменить родительский узел до того, как он будет передан распознавателю?

? за то, что не только предоставил минимальный, воспроизводимый, но и смог работать в меме там

Daniel Rearden 27.07.2019 14:44
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Что такое Apollo Client и зачем он нужен?
Что такое Apollo Client и зачем он нужен?
Apollo Client - это полнофункциональный клиент GraphQL для JavaScript-приложений, который упрощает получение, управление и обновление данных в...
1
1
297
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Один из способов сделать это — переместить логику модификации узла на уровень выше. Например, для такого типа запроса, как:

type Query {
  venues: [Venue!]!
}

мы можем просто сделать следующее внутри распознавателя:

const resolvers: {
  Query: {
    venues: async (root, args, context) => {
      const venues = await context.loaders.VenueLoader.load()
      return venues.map(magic)
    }
  }
}

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

Однако мы можем сделать еще один шаг и использовать директиву схемы. Это было бы полезно, если, например, вы хотите повторно использовать одну и ту же логику для разных типов или по какой-то странной причине хотите изменить родителя только для некоторых полей. Вот пример, который позволит вам применить директиву либо к типу, либо к отдельным полям:

class MagicDirective extends SchemaDirectiveVisitor {
  visitFieldDefinition(field) {
    const { resolve = defaultFieldResolver } = field
    field.resolve = function (source, args, context, info) {
      return resolve.apply(this, [magic(source), args, context, info])
    }
  }
  visitObject(object) {
    const fieldMap = object.getFields()
    for (const fieldName in fieldMap) {
      this.visitFieldDefinition(fieldMap[fieldName])
    }
  }
}

Затем просто передайте директиву в свою конфигурацию ApolloServer как часть schemaDirectives и включите ее в определения типов:

directive @magic on FIELD_DEFINITION | OBJECT

Никогда не требовалось достаточно времени, чтобы изучить директиву схемы до вашего ответа. Они замечательные! Мой вариант использования — реализовать шаблон распознавателя, описанный в этом посте. medium.com/paypal-engineering/… На самом деле предполагается, что родительский узел возвращает только идентификатор — объекты распознавателя должны получить свои данные. Как вы упомянули, это уменьшает дублирование кода в родительском узле.

Gajus 27.07.2019 15:22

@Gajus Спасибо, что поделились этой статьей. Этот является довольно крутой паттерн, особенно если вы уже работаете с DataLoader. FWIW, я не думаю, что использование этого шаблона требует от вас явного изменения ваших родительских значений. Если вы уже получаете «полный» объект, но вам нужен только идентификатор, вы ничего не получите от преобразования объекта, кроме дополнительных накладных расходов.

Daniel Rearden 27.07.2019 19:18

@Gajus Я пробовал этот подход, и я думаю, что в этой статье рекомендуется ОГРОМНЫЙ антипаттерн. Что произойдет, если объект не существует? Вы вернули что-то от своего родителя, поэтому вы говорите, что объект существует. Правильный ответ, если объект не существует, — null, и теперь вы сделали это невозможным.

Dan Crews 31.07.2019 23:14

Как можно что-то вернуть от родителя, если возвращать нечего?

Gajus 01.08.2019 00:23

Вот что он рекомендует: venueById(parent, args) { return { id: args.id }; }. Он принял ввод пользователя и вернул объект без поиска

Dan Crews 01.08.2019 00:30

Это просто для иллюстрации (я полагаю). Ожидается, что вы будете искать значение PK в родительском распознавателе, а затем позволите дочерним элементам разрешать любую дополнительную информацию, необходимую на основе значения PK. Ценность этого подхода заключается в том, что если у вас достаточно сложная логика DataLoader, вы можете хранить ее в распознавателе вместе со всеми другими определениями полей, и родителям больше не нужно знать о шаблоне DataLoader.

Gajus 01.08.2019 01:21

@DanCrews Может быть, мы сможем переместить этот разговор в чат

Daniel Rearden 01.08.2019 01:37

Другие вопросы по теме