在ApolloServer中支持异议`$ formatJson`

时间:2019-10-29 22:39:45

标签: node.js graphql apollo apollo-server objection.js

我是Objection.jsGraphQL的忠实拥护者,我正在努力让他们一起为我的企业开发新的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调用。我调查了formatResponse here,但看起来它已经从Model类中分离出了数据。

任何熟悉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);

这里的问题是,转换将发生在调用 之前,然后调用较低级别的解析器以更深入地遍历图形。因此,这些解析器的第一个参数将接收这些转换后的Model实例,并且这些将不再具有原始Model类(例如$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框架。

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

我发现了一种似乎可以解决问题的方法,除非有人提出更好的解决方法,否则它将很快发布答案。

3 个答案:

答案 0 :(得分:1)

因此在GraphQL中,对象不会直接转换为JSON。相反,只有少数几个地方将值转换为JSON值。那是在叶子类型里面。

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

对于枚举类型,我们可以在内部定义不同于外部定义的值。您可以将任何所需的值分配给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,则必须slightly differently。现在来看更有趣的标量案例。在您的示例中,日期可以通过日期标量序列化。

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日期!阿波罗再次是a bit different

如果您不想为此编写自己的标量类型,请查看Uri's GraphQL Scalars。或GraphQL ISO Date可以在我的示例中做很多事情。

答案 1 :(得分:0)

我发现的解决方法涉及使用与我的编辑中详述的方法相同的graphql-middleware方法,并在转换后的对象上存储对原始Model实例的引用。这些引用都以Symbol为关键字,以确保结果中的任何属性名称都不会发生冲突。

以下内容将用作我所有数据库模型的基类:

import { Model as ObjectionModel, Pojo } from 'objection';

export const entitySymbol = Symbol('Model instance');

export class Model extends ObjectionModel {
    $toJson(): Pojo {
        const result = super.$toJson();
        // Retain a reference to the original entity.
        result[entitySymbol] = this;
        return result;
    }
}

与此同时,此模块提供了可用于包装我的解析器并在必要时提取引用的功能:

import { Model, entitySymbol } from './model';
import { IFieldResolver } from 'apollo-server-koa';
import { GraphQLResolveInfo } from 'graphql';

export interface ResponseEntity<T> {
    [entitySymbol]: T;
}

export type EntityResolver<
    TEntity extends Model,
    TContext = any,
    TArgs = Record<string, any>,
> = (
    entity: TEntity,
    args: TArgs,
    context: TContext,
    info: GraphQLResolveInfo,
) => any;

export function entityResolver<
    TEntity extends Model,
    TContext = any,
    TArgs = Record<string, any>,
>(
    resolver: EntityResolver<TEntity, TContext, TArgs>,
): IFieldResolver<ResponseEntity<TEntity>, TContext, TArgs> {
    return (
        obj: ResponseEntity<TEntity>,
        args: TArgs,
        context: TContext,
        info: GraphQLResolveInfo,
    ) => resolver(obj[entitySymbol], args, context, info);

现在我可以像这样实现我的解析器了:

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

使用该符号感觉有些古怪,但似乎足够健壮,最终我看不出有什么更好的方法可以做到这一点。

答案 2 :(得分:0)

Objection具有静态方法fromJson,用于创建可以像这样使用的模型实例:

clc
clear all
close all
image=imread('Water lilies.jpg');

Q=[8 36 36 36 39 45 52 65;
36 36 36 37 41 47 56 68;
36 36 38 42 47 54 64 78;
36 37 42 50 59 69 81 98;
39 41 47 54 73 89 108 130;
45 47 54 69 89 115 144 178;
53 56 64 81 108 144 190 243;
65 68 78 98 130 178 243 255];

GrayImage=rgb2gray(image);

NewImage=uint8(zeros(size(GrayImage)));
Q=uint32(Q);

for i=1:size(GrayImage,1)/8
    for j=1:size(GrayImage,2)/8

        block=GrayImage(((i-1)*8)+1:((i-1)*8)+8,((j-1)*8)+1:((j-1)*8)+8);
        dct=dct2(block);
        dct=uint32(dct);
        a=dct./Q;

        z=a.*Q;
        idct=idct2(z);
        NewImage(((i-1)*8)+1:((i-1)*8)+8,((j-1)*8)+1:((j-1)*8)+8)=uint8(idct);
    end
end
imwrite(NewImage,'NewImage.jpg');

GrayImage=double(GrayImage);
NewImage=double(NewImage);

MSE=0;
for i=1:size(GrayImage,1)
    for j=1:size(GrayImage,2)
        d=(GrayImage(i,j)-NewImage(i,j))^2;
        MSE=d+MSE;
    end
end