在 GraphQL 中更改之前删除只读字段

问题描述 投票:0回答:3

我的架构中有一个名为

Article
的类型:

type Article {
  id: ID!
  updated: DateTime
  headline: String
  subline: String
}

对于它的更新,有一个由

updateArticle(id: ID!, article: ArticleInput!)
突变使用的相应输入类型:

input ArticleInput {
  headline: String
  subline: String
}

突变本身看起来像这样:

mutation updateArticle($id: ID!, $article: ArticleInput!) {
  updateArticle(id: $id, article: $article) {
    id
    updated
    headline
    subline
  }
}

文章始终作为一个整体保存(而不是一个接一个地保存各个字段),因此当我将一篇文章传递给我之前获取的突变时,它会抛出诸如

Unknown field. In field "updated"
Unknown field. In field "__typename"
Unknown field. In field "id"
之类的错误。这些问题的根本原因是这些字段没有在输入类型上定义。

根据规范,这是正确的行为:

(…) 这个无序映射不应该包含任何名称不是的条目 由该输入对象类型的字段定义,否则出错 应该扔掉。

现在我的问题是处理这种情况的好方法是什么。我是否应该在我的应用程序代码中列出输入类型允许的所有属性?

如果可能的话,我想避免这种情况,也许有一个实用函数可以为我切掉它们,它知道输入类型。但是,由于客户端不知道架构,因此这必须在服务器端发生。因此,不必要的属性将被转移到那里,我认为这就是为什么它们不应该首先被转移的原因。

还有比维护属性列表更好的方法吗?

我正在使用

apollo-client
react-apollo
graphql-server-express

javascript graphql graphql-js react-apollo apollo-client
3个回答
10
投票

您可以使用片段进行查询,其中包括数据的所有可变字段。 过滤器可以使用该片段在突变发生之前删除所有不需要的数据。

要点是:

const ArticleMutableFragment = gql`
fragment ArticleMutable on Article {
  headline
  subline
  publishing {
    published
    time
  }
}
`

const ArticleFragment = gql`
fragment Article on Article {
  ...ArticleMutable
  id
  created
  updated
}
${ArticleMutableFragment}
`;

const query = gql`
query Article($id: ID!) {
  article(id: $id) {
    ...Article
  }
}
${ArticleFragment}
`;

const articleUpdateMutation = gql`
mutation updateArticle($id: ID!, $article: ArticleInput!) {
  updateArticle(id: $id, article: $article) {
    ...Article
  }
}
${ArticleFragment}
`;

...

import filterGraphQlFragment from 'graphql-filter-fragment';

...

graphql(articleUpdateMutation, {
  props: ({mutate}) => ({
    onArticleUpdate: (id, article) =>
      // Filter for properties the input type knows about
      mutate({variables: {id, article: filterGraphQlFragment(ArticleMutableFragment, article)}})
  })
})

...

ArticleMutable
片段现在也可以重复用于创建新文章。


0
投票

我个人也有同样的想法,并早些时候采用了 @amann 的方法,但一段时间后,在输入类型上使用查询片段的概念缺陷变得明显。您可以选择选择(相应的)对象类型中不存在的输入类型字段 - 有吗?

目前我正在通过

typesafe-joi
模式描述我的输入数据,并使用它的
stripUnknown
选项来过滤我的表单数据。

无效数据永远不会离开表单,因此可以静态输入有效数据。

从某种意义上说,创建 joi 模式与定义“输入片段”是相同的活动,因此不会发生代码重复,并且您的代码可以是类型安全的。


0
投票

如果使用

graphql-codegen
,那么可以向他的项目添加另一个 codegen 配置,如下所示:

import { CodegenConfig } from '@graphql-codegen/cli';

import { commonConfig } from './configs/common.config';

const classesCodegen: CodegenConfig = {
  schema: 'apps/back/src/app/schema.gql',
  documents: ['apps/front/**/*.tsx'],
  ignoreNoDocuments: true,
  generates: {
    'libs/data-layer/src/lib/gql/classes.ts': {
      plugins: ['typescript'],
      config: {
        declarationKind: {
          // directive: 'type',
          // scalar: 'type',
          input: 'class',
          // type: 'type',
          // interface: 'type',
          // arguments: 'type',
        },
        ...commonConfig,
      },
    },
  },
};

export default classesCodegen;

在上面,我们要求

graphql-codegen
生成输入作为类。生成的代码将类似于:

/** Material Input */
export class MaterialInput {
  /** Material's id */
  _id?: InputMaybe<Scalars['Id']['input']>;
  /** Material's coding config */
  codingConfig: CodingConfigUnionInput;
  /** Material's content */
  content?: InputMaybe<Scalars['String']['input']>;
  /** Material's cost usages */
  costUsages: Array<CostUsageInput>;
  /** Material's label */
  label: Scalars['String']['input'];
  /** Material's status id */
  statusId: Scalars['Id']['input'];
  /** Material's title */
  title?: InputMaybe<Scalars['String']['input']>;
};

通过使用类而不是类型或接口,我们现在可以在将数据发送到后端之前对其进行修剪。

这是一个小实用程序,用于从对象中删除关于给定类的任何多余数据:

import type { C, O } from 'ts-toolbelt';
import { assign, keys, pick } from 'lodash';

export const pruneInput = <I extends object>(
  instance: O.Object,
  Class: C.Class<unknown[], I>,
): I => {
  const input = new Class();
  assign(input, pick(instance, keys(input)));
  return input;
};

最后,您可以清理数据,而无需再维护此清理步骤:

import type { O } from 'ts-toolbelt';

import { CostUsageInput, MaterialInput } from '@your-organization/data-layer';

import { pruneInput } from '../../../utils/prune-input.util';

import { codingConfigForm2ApiMapper } from '../../mappers/coding-config.form2api.mapper';

import type {
  MaterialForm_Material,
  MaterialInput as IMaterialInput,
} from '..';

export function materialForm2ApiMapper(
  material: O.Readonly<MaterialForm_Material>,
): O.Readonly<IMaterialInput> {
  const materialInput = pruneInput(material, MaterialInput);

  const costUsagesInput = materialInput.costUsages.map((costUsage) =>
    pruneInput(costUsage, CostUsageInput),
  );

  return {
    ...materialInput,
    costUsages: costUsagesInput,
    codingConfig: codingConfigForm2ApiMapper(material.codingConfig),
  };
}

© www.soinside.com 2019 - 2024. All rights reserved.