Необязательные, но не обнуляемые поля в GraphQL - PullRequest
0 голосов
/ 07 ноября 2018

При обновлении нашего GraphQL API требуется только поле моделей _id, следовательно, ! в приведенном ниже коде языка SDL. Другие поля, такие как name, не обязательно должны быть включены в обновление, но также не могут иметь значение null. В настоящее время исключение ! из поля имени позволяет конечному пользователю не пропускать name в обновлении, но позволяет ему передавать значение null для name in, что недопустимо.

Значение null позволяет узнать, что поле необходимо удалить из базы данных.

Ниже приведен пример модели, в которой это может вызвать проблемы - пользовательский скаляр Name не допускает нулевые значения, но GraphQL все еще позволяет им:

type language {
  _id: ObjectId
  iso: Language_ISO
  auto_translate: Boolean
  name: Name
  updated_at: Date_time
  created_at: Date_time
}
input language_create {
  iso: Language_ISO!
  auto_translate: Boolean
  name: Name!
}
input language_update {
  _id: ObjectId!
  iso: Language_ISO!
  auto_translate: Boolean
  name: Name
}

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

Мне известно, что ! означает non-nullable и что отсутствие ! означает, что поле можно обнулять, однако это огорчает, что, насколько я вижу, мы не можем указать точные значения для поля если поле не требуется / необязательно. Эта проблема возникает только при обновлениях.

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

ПРИМЕР МУТАЦИИ, КОТОРЫЙ ДОЛЖЕН НЕ УКАЗАТЬСЯ

mutation tests_language_create( $input: language_update! ) { language_update( input: $input ) { name  }}

Переменные

input: {
  _id: "1234",
  name: null
}

ОБНОВЛЕНИЕ 9/11/18: для справки, я не могу найти способ обойти это, поскольку есть проблемы с использованием пользовательских скаляров, пользовательских директив и правил проверки. Я открыл вопрос о GitHub здесь: https://github.com/apollographql/apollo-server/issues/1942

1 Ответ

0 голосов
/ 08 ноября 2018

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

const { specifiedRules } = require('graphql/validation')
const { GraphQLError } = require('graphql/error')

const typesToValidate = ['Foo', 'Bar']

// This returns a "Visitor" whose properties get called for
// each node in the document that matches the property's name
function CustomInputFieldsNonNull(context) {
  return {
    Argument(node) {
      const argDef = context.getArgument();
      const checkType = typesToValidate.includes(argDef.astNode.type.name.value)
      if (checkType && node.value.kind === 'NullValue') {
        context.reportError(
          new GraphQLError(
            `Type ${argDef.astNode.type.name.value} cannot be null`,
            node,
          ),
        )
      }
    },
  }
}

// We're going to override the validation rules, so we want to grab
// the existing set of rules and just add on to it
const validationRules = specifiedRules.concat(CustomInputFieldsNonNull)

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules,
})

РЕДАКТИРОВАТЬ : вышеописанное работает, только если вы не используете переменные, что в большинстве случаев не очень полезно. В качестве обходного пути я смог использовать директиву FIELD_DEFINITION для достижения желаемого поведения. Вероятно, есть несколько способов подойти к этому, но вот базовый пример:

class NonNullInputDirective extends SchemaDirectiveVisitor {
  visitFieldDefinition(field) {
    const { resolve = defaultFieldResolver } = field
    const { args: { paths } } = this
    field.resolve = async function (...resolverArgs) {
      const fieldArgs = resolverArgs[1]
      for (const path of paths) {
        if (_.get(fieldArgs, path) === null) {
          throw new Error(`${path} cannot be null`)
        }
      }
      return resolve.apply(this, resolverArgs)
    }
  }
}

Тогда в вашей схеме:

directive @nonNullInput(paths: [String!]!) on FIELD_DEFINITION

input FooInput {
  foo: String
  bar: String
}

type Query {
  foo (input: FooInput!): String @nonNullInput(paths: ["input.foo"])
}

Предполагая, что «ненулевые» поля ввода одинаковы каждый раз, когда input используется в схеме, вы можете сопоставить имя каждого input с массивом имен полей, которые должны быть проверены. Так что вы можете сделать что-то вроде этого:

const nonNullFieldMap = {
  FooInput: ['foo'],
}

class NonNullInputDirective extends SchemaDirectiveVisitor {
  visitFieldDefinition(field) {
    const { resolve = defaultFieldResolver } = field
    const visitedTypeArgs = this.visitedType.args
    field.resolve = async function (...resolverArgs) {
      const fieldArgs = resolverArgs[1]
      visitedTypeArgs.forEach(arg => {
        const argType = arg.type.toString().replace("!", "")
        const nonNullFields = nonNullFieldMap[argType]
        nonNullFields.forEach(nonNullField => {
          const path = `${arg.name}.${nonNullField}`
          if (_.get(fieldArgs, path) === null) {
            throw new Error(`${path} cannot be null`)
          }
        })
      })      

      return resolve.apply(this, resolverArgs)
    }
  }
}

А потом в вашей схеме:

directive @nonNullInput on FIELD_DEFINITION

type Query {
  foo (input: FooInput!): String @nonNullInput
}
...