首页 > 解决方案 > 支持 ApolloServer 中的 Objection `$formatJson`

问题描述

我是Objection.jsGraphQL的忠实粉丝,我正试图让它们在我的业务中为新的 API 一起工作。不幸的是,我遇到了一些困难。

关于 Objection 的最好的事情之一是模型数据生命周期,它允许您在模型类中指定如何在各种格式之间转换其属性。您可以以一种方式将其存储在数据库中,以另一种方式在代码中使用它,并在以另一种方式将其发送到客户端之前对其进行序列化。

例如, $formatJson可用于在您使用模型时更改作为 JS Date 对象的 Date 属性,但在响应中发送时会转换为 ISO 字符串:

$formatJson(json: Pojo): Pojo {
    json = super.$formatJson(json);
    if (json.lastActive) json.lastActive = json.lastActive.toISOString();
    return json;
}

此方法在实例的#toJSON方法中调用,通常在按此处所述进行字符串化时调用该方法。

然而,ApolloServer(特别是我正在使用的 apollo-server-koa )并没有直接对这些模型实例进行字符串化。它似乎(合理地)将属性子集复制到新对象,将数据与其实例方法分开。因此,#$formatJson永远不会被调用,并且我的日期作为时间戳返回,因为这就是默认情况下 JS 日期字符串化的方式。

似乎需要以某种方式#toJSON在解析器函数和从它们的返回值复制属性之间注入一些调用。我查看了formatResponsehere,但看起来它已经与模型类分离后接收数据。

熟悉 ApolloServer 的人能指出我正确的方向吗?我需要研究某种插件 API 吗?

我发现objection-graphql非常酷,但看起来它通过在顶级解析器中处理整个查询并#toJSON在解析器返回之前递归调用所有内容来处理这个问题。默认解析器会从预先加载的结构中提取所有其他内容。超级酷,但它看起来不够灵活,无法满足我的需求。真的,我只是想解决这个特定的问题,而不是让我的整个 api 变魔术。:\


编辑:

我已经对此进行了更多研究,现在我想更清楚地说明这个问题,因为我已经更好地掌握了 GraphQL 的实际实现方式。

GraphQL 通过调用解析器并逐个属性地组装响应来工作。首先调用您的顶级解析器,然后在它们返回后,调用下一层的每个属性的解析器,依此类推,直到只剩下叶子。每个叶子的返回值最终分配到一个对象结构中,该结构被字符串化为 JSON。这意味着这些属性最初来自的对象上的任何实例方法都将在此时完全丢失。因此,如果您希望使用这些实例方法(在本例中为 Objection's )对整个结果进行某种“最终”转换$formatJSON,您将遇到一些困难。

困难

可以使用graphql-middleware来拦截任何解析器的结果并递归地调用您对其内容的转换,使用如下所示:

import { isArray, isObjectLike, map, mapValues } from 'lodash';
import { resolvers, typeDefs } from './api';
import { ApolloServer } from 'apollo-server-koa';
import Koa from 'koa';
import { Model } from 'objection';
import { applyMiddleware } from 'graphql-middleware';

function toResponse(value: any): any {
    // If this isn't an object or an array, just return it.
    if (!isObjectLike(value)) return value;

    // If this is an instance of Model, convert it using #$toJson
    if (value instanceof Model) return value.$toJson();

    // Create a recursive mapping function.
    const mapFn = (item: any) => toResponse(item);

    /*
     * If this an array, convert each one of its items with the mapping
     * function.
     */
    if (isArray(value)) return map(value, mapFn);

    /*
     * Otherwise, this must be an object. Convert its values with the mapping
     * function.
     */
    return mapValues(value, mapFn);
}

const schema = applyMiddleware(
    makeExecutableSchema({ typeDefs, resolvers }),
    async(
        resolve,
        root,
        args,
        info,
    ) => toResponse(await resolve(root, args, ctx, info)),
);

const app = new Koa();
const server = new ApolloServer({ schema });
server.applyMiddleware({ app });

app.listen(3000);

这里的问题是转换将在调用较低级别的解析器以更深入地遍历图形之前发生。因此,这些解析器的第一个参数将接收这些转换后的 Model 实例,并且这些实例将不再具有$relatedQuery您可能想要实现解析器的原始 Model 类(如 )的任何有用的实例方法。

例如,我可能有一个像这样的简单模式:

type Query {
    person(id: Int!): Person
}

type Person {
    id: Int!
    name: String!
    children: [Person!]
}

并像这样实现我的解析器:

{
    Query: {
        person: (
            root: undefined,
            args: { id: number },
        ) => Person.query().findById(args.id),
    },
    Person: {
        children: (person: Person) => person.$relatedQuery('children'),
    },
}

使用上面显示的中间件设置,Person.children 的解析器将不起作用,因为它的person参数不会接收 的实例Model,因此它没有$relatedQuery实例方法。

为什么:

为什么会有人想要这个?除了只是想使用 Objection 的最佳功能之一(正如 Herku 正确指出的那样,在这种情况下,使用更特定于 GQL 的东西可能会更好地处理它),还有与其他库集成的可能性,这些库对关于Objection 所做的 API 框架。

为了简单起见,我最初忽略了这一点,但我还有一个(目前是专有的)权限库,它能够过滤查询结果以删除基于各种因素不允许用户查看的属性。我目前计划集成它的方式还涉及需要原始模型实例的模型的“后解析器”转换。我可能会也可能不会走这条路,目前我不想分享太多关于这个库是如何工作的,但希望这能让情况更清楚一些。

我找到了一种似乎可行的变通方法,并且很快就会发布答案,除非其他人想出更好的方法。

标签: node.jsgraphqlapolloapollo-serverobjection.js

解决方案


所以在 GraphQL 中,对象不会直接转换为 JSON。相反,只有少数地方将值转换为 JSON 值。那是叶子类型的内部。

叶类型是 GraphQL 中没有子选择的类型。具体来说,这些是ScalarEnum。让我们看看我们如何在内部使用值但在外部使用不同的值:

对于枚举类型,我们可以在内部和外部定义不同的值。您可以将任何您想要的内容分配给 value 属性:

import { GraphQLEnumType } from 'graphql';

const MyEnum = new GraphQLEnumType({
  name: 'MyEnum',
  values: {
    VALUE_A: {
      value: 'value-a',
    },
    VALUE_B: {
      value: 'value-b',
    }
  }
});

如果你使用 Apollo,graphql-tools你必须做的稍有不同。现在来看更有趣的 Scalar 案例。在您的示例中,日期可以由日期标量序列化。

import { GraphQLScalarType } from 'graphql';
import { Kind } from 'graphql/language';

const Date = new GraphQLScalarType({
  name: 'Date',
  description: 'Date custom scalar type',
  parseValue(value) {
    return new Date(value); // value from JSON (from variable)
  },
  serialize(value) {
    return value.toISOString(); // serialization happens here
  },
  parseLiteral(ast) {
    if (ast.kind === Kind.STRING) {
      return new Date(ast.value); // value inlined in query
    }
    return null;
  },
});

现在我们序列化为 ISO 字符串并在内部使用 JavaScript 日期!阿波罗再次有点不同

如果您不想为此编写自己的 Scalar 类型,请查看Uri 的 GraphQL Scalars。或者GraphQL ISO Date,它在我的示例中做了很多事情。


推荐阅读