apollostack/graphql-server - 如何从解析器获取查询中请求的字段

apollostack/graphql-server - how to get the fields requested in a query from resolver

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

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

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

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

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

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

// 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() 搜索它,但感觉有点脏。据我所知,info.fieldASTs[0].selectionSet.selections 有字段列表,我可以检查它是否存在。我不确定这有多可靠。尤其是当我开始使用更复杂的查询时。

有没有更简单的方法?

如果您不使用 mongDB,投影是您传递的一个附加参数,用于明确告诉它要检索的内容:

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

一如既往,感谢出色的社区。

当然可以。这实际上与基于 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 用户看到一个 projection-monster 包 :)

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

使用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,
});

2020 年 1 月回答

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

该库“", and is recommended 由其他顶级库的作者继续解析 info 字段,graphql-fields

您可以从 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;

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

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]);

  return user;
};

辅助功能。

几点:

  • gql_uid用作ID!来自主键的字符串类型不更改数据库类型

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

  • allowedFields 用于过滤信息中的附加字段,例如“__typename”

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

    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(),
       };
    

    };