【问题标题】:apollostack/graphql-server - how to get the fields requested in a query from resolverapollostack/graphql-server - 如何从解析器获取查询中请求的字段
【发布时间】:2017-04-02 21:25:37
【问题描述】:

我正在尝试找出一种简洁的方式来处理查询和 mongdb 投影,这样我就不必从数据库中检索过多的信息。 所以假设我有:

// the query
type Query {
  getUserByEmail(email: String!): User
}

为了简单起见,我有一个 User 和一个 email 和一个 username。如果我发送查询并且只想检索电子邮件,我可以执行以下操作:

query { getUserByEmail(email: "test@test.com") { email } }

但是在解析器中,我的数据库查询仍然检索到usernameemail,但只有其中一个被阿波罗服务器作为查询结果传回。

我只希望数据库检索查询要求的内容:

// the resolver
getUserByEmail(root, args, context, info) {
  // check what fields the query requested
  // create a projection to only request those fields
  return db.collection('users').findOne({ email: args.email }, { /* projection */ });
}

当然问题是,获取有关客户请求的信息并不是那么简单。

假设我将请求作为上下文传递 - 我考虑使用具有查询字符串的 context.payload (hapi.js),并通过各种 .split()s 搜索它,但这感觉有点脏。据我所知,info.fieldASTs[0].selectionSet.selections 有字段列表,我可以检查它是否存在。我不确定这有多可靠。尤其是当我开始使用更复杂的查询时。

有没有更简单的方法?

如果您不使用 mongDB,投影是您传入的附加参数,明确告诉它要检索什么:

// telling mongoDB to not retrieve _id
db.collection('users').findOne({ email: 'test@test.com' }, { _id: 0 })

一如既往,感谢这个了不起的社区。​​p>

【问题讨论】:

  • 好的。所以现在还不清楚你在问什么。该查询说“请返回email”。 “但是在解析器中,我的数据库查询仍然检索两者,但只传回一个。我只希望数据库检索查询要求的内容”是什么意思?您应该共享此查询的解析器代码。
  • 这也是我的错。我应该更清楚一点。我试图找出查询要查找的字段,以便我可以使我的数据库查询仅请求查询请求的信息。我将编辑我的问题以更好地反映这一点。
  • 很抱歉很密集。仍然不清楚“查询请求的字段”是什么意思。这些领域是什么?查询是如何请求他们的?您的问题实际上是“如何进行包含有关我要执行的投影的信息的查询”?阅读这个问题听起来您认为查询已经告诉解析器“要投影的字段”是什么。您说“获取有关客户要求的信息并不简单”。实际上是的。客户要求的一切都在查询中。如果您想请求更多,请将其放在查询中。
  • 为了使用投影,我需要知道查询要求哪些字段:getUserByEmail(email: "someemail") { field }。也可以进行相同的查询:getUserByEmail(email: "someemail") { field1 field2 field3 }。如果我运行第一个查询,我需要执行db.collection('test').findOne({ args }, { field: 1 }),但对于第二个查询我需要执行db.collection('test').findOne({ args }, { field1: 1, field2: 1, field3: 1 })。我的问题是如何从解析器中获取该字段列表。
  • 我终于明白了这个问题:) 我认为你做不到。它可能依赖于实现,但使用apollo-server,您已经定义了查询模式。你的getUserByEmail 返回一个User:就是这样。似乎要求数据库提供比这更少的信息是过早的优化。为什么不直接获取用户并完成它。在客户端apollo-client 将缓存结果,因此下次如果您只有电子邮件,它会将其提供给您。

标签: mongodb hapijs graphql apollo-server


【解决方案1】:

2020 年 1 月答案

获取 GraphQL 查询中请求的字段的当前答案是使用 graphql-parse-resolve-info 库来解析 info 参数。

该库是“a pretty complete solution and is actually used under the hood by postgraphile”,是recommended,由另一个用于解析info字段的顶级库的作者graphql-fields

【讨论】:

    【解决方案2】:

    使用graphql-fields

    Apollo 服务器示例

    const rootSchema = [`
    
        type Person {
            id: String!
            name: String!
            email: String!
            picture: String!
            type: Int!
            status: Int!
            createdAt: Float
            updatedAt: Float
        }
    
        schema {
        query: Query
        mutation: Mutation
        }
    
    `];
    
    const rootResolvers = {
    
    
        Query: {
    
            users(root, args, context, info) {
                const topLevelFields = Object.keys(graphqlFields(info));
                return fetch(`/api/user?fields=${topLevelFields.join(',')}`);
            }
        }
    };
    
    const schema = [...rootSchema];
    const resolvers = Object.assign({}, rootResolvers);
    
    // Create schema
    const executableSchema = makeExecutableSchema({
        typeDefs: schema,
        resolvers,
    });
    

    【讨论】:

    • 请注意graphql-fields 的作者刚刚推荐使用graphql-parse-resolve-info
    【解决方案3】:

    当然可以。这实际上与在基于 SQL 的数据库的 join-monster 包上实现的功能相同。他们的创建者有一个谈话:https://www.youtube.com/watch?v=Y7AdMIuXOgs

    看看他们的 info 分析代码以帮助您入门 - https://github.com/stems/join-monster/blob/master/src/queryASTToSqlAST.js#L6-L30

    很想为我们 mongo 用户看到一个投影怪物包:)

    更新: 有一个包可以在 npm 上从info 创建一个投影对象:https://www.npmjs.com/package/graphql-mongodb-projection

    【讨论】:

    • 链接已损坏。
    • 实际答案是什么? '当然可以',或'info.fieldASTs[0].selectionSet.selections' 可以依赖
    • 这个答案可以通过代码示例大大改进。
    【解决方案4】:

    您可以从 info 参数生成 MongoDB 投影。这是您可以遵循的示例代码

     /**
     * @description - Gets MongoDB projection from graphql query
     *
     * @return { object }
     * @param { object } info
     * @param { model } model - MongoDB model for referencing
     */
    
    function getDBProjection(info, model) {
      const {
        schema: { obj }
      } = model;
      const keys = Object.keys(obj);
      const projection = {};
    
      const { selections } = info.fieldNodes[0].selectionSet;
    
      for (let i = 0; i < keys.length; i++) {
        const key = keys[i];
        const isSelected = selections.some(
          selection => selection.name.value === key
        );
    
        projection[key] = isSelected;
      }
    
      console.log(projection);
    }
    
    module.exports = getDBProjection;
    
    

    【讨论】:

      【解决方案5】:

      通过一些辅助函数,您可以像这样使用它(打字稿版本):

      import { parceGqlInfo, query } from "@backend";
      import { GraphQLResolveInfo } from "graphql";
      
      export const user = async (parent: unknown, args: unknown, ctx: unknown, info: GraphQLResolveInfo): Promise<User | null> => {
        const { dbQueryStr } = parceGqlInfo(info, userFields, "id");
      
        const [user] = await query(`SELECT ${dbQueryStr} FROM users WHERE id=$1;`, [1]);
      
        return user;
      };
      

      辅助函数。

      几点:

      • gql_uid 用作 ID!字符串类型从主键到不改变数据库类型

      • 必填选项用于数据加载器(如果用户未请求该字段)

      • allowedFields 用于从诸如“__typename”之类的信息中过滤其他字段

      • 如果您需要为选定字段添加前缀,例如 select u.id from users u,则使用 queryPrefix

        const userFields = [
               "gql_uid",
               "id",
               "email"
             ]
        
         // merge arrays and delete duplicates
         export const mergeDedupe = <T>(arr: any[][]): T => {
           // @ts-ignore
           return ([...new Set([].concat(...arr))] as unknown) as T;
         };
        
         import { parse, simplify, ResolveTree } from "graphql-parse-resolve-info";
         import { GraphQLResolveInfo } from "graphql";
        
         export const getQueryFieldsFromInfo = <Required = string>(info: GraphQLResolveInfo, options: { required?: Required[] } = {}): string[] => {
           const { fields } = simplify(parse(info) as ResolveTree, info.returnType) as { fields: { [key: string]: { name: string } } };
        
           let astFields = Object.entries(fields).map(([, v]) => v.name);
        
           if (options.required) {
             astFields = mergeDedupe([astFields, options.required]);
           }
        
           return astFields;
         };
        
         export const onlyAllowedFields = <T extends string | number>(raw: T[] | readonly T[], allowed: T[] | readonly T[]): T[] => {
           return allowed.filter((f) => raw.includes(f));
         };
        
         export const parceGqlInfo = (
           info: GraphQLResolveInfo,
           allowedFields: string[] | readonly string[],
           gqlUidDbAlliasField: string,
           options: { required?: string[]; queryPrefix?: string } = {}
         ): { pureDbFields: string[]; gqlUidRequested: boolean; dbQueryStr: string } => {
           const fieldsWithGqlUid = onlyAllowedFields(getQueryFieldsFromInfo(info, options), allowedFields);
        
           return {
             pureDbFields: fieldsWithGqlUid.filter((i) => i !== "gql_uid"),
             gqlUidRequested: fieldsWithGqlUid.includes("gql_uid"),
             dbQueryStr: fieldsWithGqlUid
               .map((f) => {
                 const dbQueryStrField = f === "gql_uid" ? `${gqlUidDbAlliasField}::Text AS gql_uid` : f;
        
                 return options.queryPrefix ? `${options.queryPrefix}.${dbQueryStrField}` : dbQueryStrField;
               })
               .join(),
           };
        

        };

      【讨论】:

        猜你喜欢
        • 2018-06-08
        • 2021-05-02
        • 2017-05-28
        • 2019-09-10
        • 2018-08-04
        • 2017-05-10
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多