Объяснение различных реализаций функции разрешения в graphql

Я читал документы graphQL и обнаружил, что они объясняют реализацию сервера graphql двумя способами: один использует graphql-yoga, который является полнофункциональным сервером graphql, а другой использует graphql, express-graphql и экспресс . В обоих случаях мы передаем функции схемы и преобразователя при создании экземпляра сервера.

Но реализация функции распознавателя отличается. При использовании graphql-йоги функция распознавателя получает 4 аргумента, которые содержат информацию о родительском объекте, полученных аргументах, контексте, информации. тогда как в другом случае (с использованием graphql) функция разрешения получает только объект аргументов.

Почему это так ? Если мне нужна информация, контекстные объекты, как мне ее получить?

Используя пример графл-йоги: https://graphql.org/learn/execution/

Используя пример graphql: https://graphql.github.io/graphql-js/mutations-and-input-types/

// Пример кода с использованием graphql

var express = require('express');
var graphqlHTTP = require('express-graphql');
var { buildSchema } = require('graphql');

var schema = buildSchema(`
type Query {
    rollDice(numDice: Int!, numSides: Int): [Int]
}
type Mutation {
    addDice(numDice: Int): String
}
`);

var root = {
    rollDice({numDice, numSides}) {
        return [1, 2];
    },
    addDice({numDice}) {
        console.info("Adding something");
        return "Added";
    }
};

var app = express();
app.use('/graphql', graphqlHTTP({
    schema: schema,
    rootValue: root,
    graphiql: true,
}));
app.listen(4000);
console.info('Running a GraphQL API server at localhost:4000/graphql');

// Пример кода с использованием graphql-йоги

let graphqlServer = require("graphql-yoga");

const typeDefs = `
    type Query {
        rollDice(numDice: Int!, numSides: Int): [Int]
    }
    type Mutation {
        addDice(numDice: Int): String
    }
    `;

const resolvers = {
    Query: {
        rollDice(parent, args, context, info) {
            console.info(args.numDice);
            console.info(args.numSides);
            return [1, 2];
        }
    },
    Mutation: {
        addDice(parent, args, context, info) {
            console.info(args.numDice);
            return "Added";
        }
    }
};

const server = new graphqlServer.GraphQLServer({
    typeDefs,
    resolvers
});

server.start(() => {
    console.info("server started on localhost:4000");
});

Разница между этими двумя фрагментами кода:

Функции распознавателя присутствуют внутри соответствующих типов (например, Query, Mutation) в одном случае. В другом случае они присутствуют внутри одного корневого объекта. Это означает, что в первом случае у меня могут быть методы с одинаковыми именами в Query и Mutation, тогда как во втором случае это невозможно, поскольку они являются ключами одного объекта, а ключи должны быть уникальными.

Почему это так? Я в принципе что-то упускаю? Как детали реализации могут отличаться от одного пакета к другому?

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

Ответы 1

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

СЕРЬЕЗНЫЙ РАЗГОВОР: документы GraphQL.js не так хороши. На мой взгляд, они никогда не должны были использовать примеры с buildSchema, потому что это по понятным причинам приводит к такой путанице.

GraphQL.js (то есть пакет graphql) — это реализация GraphQL на JavaScript. Построение схемы в GraphQL.js выполняется программно путем создания экземпляра класса GraphQLSchema:

const userType = new GraphQLObjectType({
  name: 'User',
  fields: {
    id: {
      type: GraphQLID,
    },
    email: {
      type: GraphQLString,
    },
  },
});
const queryType = new GraphQLObjectType({
  name: 'Query',
  fields: {
    user: {
      type: userType,
      resolve: () => ({ id: 1, email: '[email protected]' }),
    },
  },
});
const schema = new GraphQLSchema({
  query: queryType,
})

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

type Query {
  user: User
}

type User {
  id: ID
  email: String
}

Работать с SDL гораздо проще, чем писать весь этот код. Однако GraphQL.js не позволяет создать полнофункциональную схему из SDL. Это делает предоставляет функцию buildSchema, но эта утилита создает схему без каких-либо резольверов (и ряд других функций, таких как разрешение типов объединения/интерфейса).

Пакет graphql-tools предоставляет функцию makeExecutableSchema, которая позволяет создавать схему из SDL и объекта карты преобразователя. Это то, что используется под капотом apollo-server и graphql-yoga. makeExecutableSchema создает схему из SDL, используя buildSchema, а затем мутирует полученный объект, добавляя распознаватели в постфактум.

В GraphQL.js функция resolve (или преобразователь) для поля принимает четыре параметра — родительское значение, аргументы поля, контекст и объект GraphQLResolveInfo. Если мы создаем GraphQLObjectType как userType в приведенном выше примере, это необязательная функция, которую мы можем предоставить для каждого из полей в нашем объекте. Это функция такой же, которую вы определяете, когда создаете карту преобразователя для использования с graphql-yoga. Это единственная реализация преобразователя полей.

Так что там с buildSchema??

Примеры в документах используют GraphQL преобразователь полей по умолчанию:

export const defaultFieldResolver: GraphQLFieldResolver<any, *> = function(
  source,
  args,
  contextValue,
  info,
) {
  if (typeof source === 'object' || typeof source === 'function') {
    const property = source[info.fieldName];
    if (typeof property === 'function') {
      return source[info.fieldName](args, contextValue, info);
    }
    return property;
  }
};

Как видите, логика разрешения по умолчанию ищет свойство с тем же именем, что и поле в исходном (родительском) значении. В нашем примере выше преобразователь user возвращает {id: 1, email: '[email protected]'} — это значение, которому соответствует поле решает. Поле имеет тип User. У нас нет определенного преобразователя для нашего поля id, поэтому преобразователь по умолчанию делает свое дело. Поле id преобразуется в 1, потому что это значение свойства с именем id в родительском объекте, которое получает распознаватель.

Однако родительское значение также может быть функцией, а не объектом. Если это функция, она вызывается первой, а затем используется возвращаемое значение. С чем вызывается функция? Ну, он не может передать ему родительское значение (из-за бесконечной рекурсии), но он могу передает ему оставшиеся три параметра (args, context и info). Вот что он делает.

Теперь о фокусе ??

В нашем примере я могу опустить преобразователь для поля user и вместо этого передать функцию корневому значению.

const root = {
  user: () => ({id: 1, email: '[email protected]'})
}

Корневой объект — это просто необязательный объект, который передается в качестве родительского значения преобразователям на корневом уровне (например, вашим типам Query или Mutation). В противном случае эти преобразователи не имели бы родительского значения.

Query — это операционный корневой тип — он служит «точкой входа» в остальную часть вашей схемы. Любым полям типа Query будет передан корневой объект в качестве родительского значения. Если я пропущу преобразователь для поля user, преобразователь по умолчанию 1) проверит родительский объект на наличие свойства с тем же именем, 2) найдет свойство и определит, что это функция, 3) вызовет функцию, 4) разрешит поле в возвращаемое значение функции.

ТАДА!

Однако, поскольку функция вызывается распознавателем по умолчанию и не используется как распознаватель, она получит только три вышеупомянутых параметра вместо 4.

Это удобный способ обойти невозможность фактически предоставить пользовательские преобразователи для схемы, но он очень ограничен. Он работает только для корневых типов, поэтому мы не можем предоставить поддельные преобразователи для полей User или других типов. Мы не можем использовать интерфейсы или объединения в нашей схеме, потому что мы не можем предоставить resolveType функции. И так далее...

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

Спасибо @daniel-rearden Это дало мне некоторую ясность. Надеюсь, они скоро обновят документы xD

Naveen Manikannan 26.04.2019 08:22

Я искал эту точную информацию уже более полного дня. Это отличное резюме, которое устраняет все эти недоразумения в отношении распознавателей по умолчанию, buildSchema и makeExecutableSchema и т. д. Большое спасибо, Дэниел. Я полностью согласен с тем, что многое из этого должно быть в документах.

Noah 26.01.2021 19:45

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