GraphQL中的可选但非空字段

Mat*_*w P 3 node.js graphql graphql-js apollo-server

在更新我们的GraphQL API时,仅_id需要model 字段,因此!在下面的SDL语言代码中。其他字段(例如,name不必包含在更新中)也不能具有null值。当前,!从名称字段中排除namein 允许最终用户不必在更新中传递in,但允许最终用户传递in的nullname,这是不允许的。

一个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
}
Run Code Online (Sandbox Code Playgroud)

当传入null值时,它会绕过我们的Scalars,因此如果null不是允许的值,我们就不会引发用户输入验证错误。

我知道这!意味着non-nullable,并且缺少!手段意味着该字段可为空,但是令人沮丧的是,据我所知,如果某个字段不是必需的/可选的,我们无法为该字段指定确切的值。仅在更新时会发生此问题。

有什么方法可以通过自定义Scalars解决此问题,而不必在每个更新解析器中启动硬编码逻辑,而这似乎很麻烦?

可能失败的示例变异

mutation tests_language_create( $input: language_update! ) { language_update( input: $input ) { name  }}
Run Code Online (Sandbox Code Playgroud)

变数

input: {
  _id: "1234",
  name: null
}
Run Code Online (Sandbox Code Playgroud)

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

Dan*_*den 6

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

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,
})
Run Code Online (Sandbox Code Playgroud)

编辑:以上内容仅在您不使用变量的情况下有效,这在大多数情况下不会很有帮助。解决方法是,我能够利用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)
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

然后在您的架构中:

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

input FooInput {
  foo: String
  bar: String
}

type Query {
  foo (input: FooInput!): String @nonNullInput(paths: ["input.foo"])
}
Run Code Online (Sandbox Code Playgroud)

假设每次input在架构中使用“ null”输入字段都相同时,可以将每个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)
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

然后在您的架构中:

directive @nonNullInput on FIELD_DEFINITION

type Query {
  foo (input: FooInput!): String @nonNullInput
}
Run Code Online (Sandbox Code Playgroud)