Предположим, у нас есть эта схема 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);
},
};
Но этого (насколько я могу судить) не существует.
Как изменить родительский узел до того, как он будет передан распознавателю?


Один из способов сделать это — переместить логику модификации узла на уровень выше. Например, для такого типа запроса, как:
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 Спасибо, что поделились этой статьей. Этот является довольно крутой паттерн, особенно если вы уже работаете с DataLoader. FWIW, я не думаю, что использование этого шаблона требует от вас явного изменения ваших родительских значений. Если вы уже получаете «полный» объект, но вам нужен только идентификатор, вы ничего не получите от преобразования объекта, кроме дополнительных накладных расходов.
@Gajus Я пробовал этот подход, и я думаю, что в этой статье рекомендуется ОГРОМНЫЙ антипаттерн. Что произойдет, если объект не существует? Вы вернули что-то от своего родителя, поэтому вы говорите, что объект существует. Правильный ответ, если объект не существует, — null, и теперь вы сделали это невозможным.
Как можно что-то вернуть от родителя, если возвращать нечего?
Вот что он рекомендует: venueById(parent, args) { return { id: args.id }; }. Он принял ввод пользователя и вернул объект без поиска
Это просто для иллюстрации (я полагаю). Ожидается, что вы будете искать значение PK в родительском распознавателе, а затем позволите дочерним элементам разрешать любую дополнительную информацию, необходимую на основе значения PK. Ценность этого подхода заключается в том, что если у вас достаточно сложная логика DataLoader, вы можете хранить ее в распознавателе вместе со всеми другими определениями полей, и родителям больше не нужно знать о шаблоне DataLoader.
@DanCrews Может быть, мы сможем переместить этот разговор в чат
? за то, что не только предоставил минимальный, воспроизводимый, но и смог работать в меме там