【问题标题】:Optional but non-nullable fields in GraphQLGraphQL 中的可选但不可为空的字段
【发布时间】:2019-04-11 03:46:39
【问题描述】:

在我们的 GraphQL API 更新中,只有模型 _id 字段是必需的,因此以下 SDL 语言代码中的 ! 是必需的。 name 等其他字段不必包含在更新中,但也不能有 null 值。目前,从名称字段中排除! 允许最终用户不必在更新中传递name,但它允许他们为name 传递null 值,这是不允许的。

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
}

2018 年 9 月 11 日更新: 作为参考,我找不到解决方法,因为使用自定义标量、自定义指令和验证规则存在问题。我在 GitHub 上打开了一个问题:https://github.com/apollographql/apollo-server/issues/1942

【问题讨论】:

标签: node.js graphql graphql-js apollo-server


【解决方案1】:

您实际上正在寻找的是自定义验证逻辑。您可以在构建模式时通常包含的“默认”集之上添加所需的任何验证规则。下面是一个粗略的示例,说明如何添加一个规则,在将特定类型或标量用作参数时检查它们的空值:

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
}

【讨论】:

  • 这是个好主意 - 谢谢!我对 argDef.astNode.type.name.value 有一个问题,因为它总是返回“Variable”——我认为这可能与使用变量的突变有关(参见添加到问题中的示例)。如果我能获得传入的字段的值,我可以将上面的内容修改为我需要的值,但对于我的生活,我无法在上下文或节点变量中找到它。非常感谢您的帮助!
  • 只是为了添加更多上下文,我们实际上正在使用可以明确为空的标量,即“NAME_or_Null”-“String_or_Null”。如果它不包含“_or_Null”,那么传入的值不能为 null,或者至少从外部开发人员的角度来看,这似乎是一种明智的方法
  • @MatthewP 所以我没有意识到变量值实际上是在执行时验证的,这意味着我认为验证规则无法访问它们。这使得该解决方案不适用于您的特定情况。将不得不考虑更多,但在我的脑海中,将替换您的整个输入类型的自定义标量可能更可行。
  • 啊好吧!我已经尝试过自定义标量路线,但空值似乎绕过了标量......有点痛苦!无论如何,感谢您的宝贵时间!
猜你喜欢
  • 2020-01-09
  • 2023-02-18
  • 2021-10-29
  • 2021-11-27
  • 2021-10-24
  • 2019-10-25
  • 2022-01-02
  • 2019-07-08
  • 2020-05-23
相关资源
最近更新 更多