【发布时间】:2020-02-25 05:15:37
【问题描述】:
我是Objection.js 和GraphQL 的忠实拥护者,我正努力让他们一起为我的业务开发一个新的API。不幸的是,我遇到了一些困难。
Objection 的一大优点是model data lifecyle,它允许您在模型类中指定如何在各种格式之间转换其属性。您可以以一种方式将其存储在数据库中,以另一种方式在代码中使用它,并以另一种方式在将其发送给客户端之前对其进行序列化。
例如,$formatJson 可用于在您使用模型时更改作为 JS Date 对象的 Date 属性,但在响应中发送时会转换为 ISO 字符串:
$formatJson(json: Pojo): Pojo {
json = super.$formatJson(json);
if (json.lastActive) json.lastActive = json.lastActive.toISOString();
return json;
}
此方法在实例的#toJSON 方法中调用,通常在按照here 所述进行字符串化时调用。
ApolloServer(特别是我正在使用的apollo-server-koa)并没有直接对这些模型实例进行字符串化。它似乎(合理地)将属性子集复制到新对象,将数据与其实例方法分开。因此,#$formatJson 将永远不会被调用,并且我的日期作为时间戳返回,因为这就是 JS 日期默认字符串化的方式。
似乎需要以某种方式在解析器函数和从它们的返回值复制属性之间注入一些#toJSON 调用。我查看了formatResponsehere,但看起来它已经从模型类中分离出来后接收数据。
任何熟悉 ApolloServer 的人能指出我正确的方向吗?我需要研究某种插件 API 吗?
我发现objection-graphql 非常酷,但看起来它通过在顶级解析器中处理整个查询并在解析器返回之前对所有内容递归调用#toJSON 来处理这个问题。默认解析器会从预先加载的结构中提取所有其他内容。超级酷,但它看起来不够灵活,无法满足我的需求。真的,我只是想解决这个特定的问题,而不是让我的整个 api 变魔术。 :\
编辑:
我对此进行了更多研究,现在我想更清楚地说明这个问题,因为我已经更好地掌握了 GraphQL 的实际实现方式。
GraphQL 通过调用解析器并逐个属性地组装响应来工作。首先调用您的顶级解析器,然后在它们返回后,调用下一层的每个属性的解析器,依此类推,直到只剩下叶子。每个叶子的返回值最终分配到一个对象结构中,该结构被字符串化为 JSON。这意味着这些属性最初来自的对象上的任何实例方法都将在此时完全丢失。因此,如果您希望使用这些实例方法(在本例中为 Objection 的 $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);
这里的问题是,转换将发生在您的较低级别解析器被调用以更深入地遍历图表之前。因此,这些解析器的第一个参数将接收这些转换后的模型实例,这些将不再具有您可能想要的原始模型类的任何有用的实例方法(如$relatedQuery)实现解析器。
例如,我可能有一个像这样的简单架构:
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.js graphql apollo apollo-server objection.js