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

时间:2016-11-18 22:42:53

标签: mongodb hapijs graphql apollo-server

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

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

我有User emailusername,以保持简单。如果我发送查询而我只想检索电子邮件,我可以执行以下操作:

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 })

一如既往,感谢这个令人惊叹的社区。

5 个答案:

答案 0 :(得分:2)

当然可以。这实际上与基于SQL的db的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:https://www.npmjs.com/package/graphql-mongodb-projection

上从info创建投影对象

答案 1 :(得分: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,
});

答案 2 :(得分: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

答案 3 :(得分:0)

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

答案 4 :(得分:0)

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

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

    };