使用Node.js实现GraphQL API

首先

我将在Node.js中实现GraphQL API。关于GraphQL的概述以及客户端模式的描述,我看到了一些日文文档。然而,关于API实现的文档却不太常见,所以我会写下来。

我将介绍我自己进行研究并实施的样例。如果您能参考其中一例,我将感到非常幸运。如果您有更好的实施方法,请告诉我。

不提及GraphQL的概述。官方网站和下面的文章已经介绍了相关内容。(第二篇文章的后半部分还介绍了在Ruby中进行API实现的方法。)

    • GraphQL入門 – 使いたくなるGraphQL

 

    「GraphQL」徹底入門 ─ RESTとの比較、API・フロント双方の実装から学ぶ

制作物(概要设计)

语言框架

    • TypeScript

 

    • Node.js

 

    express

我們將在Node.js的Web框架express上創建一個運行的GraphQL API。

组成

构成如下图所示。MySQL被设置在数据库中,GraphQL API对MySQL进行包装。API将GraphQL请求转换为常规的SQL查询,并向数据库发出查询,并将结果作为响应返回。

composion.png

题材

假设有一个电商网站的数据库。这个数据库按照以下结构进行管理:客户信息、产品信息、制造商信息和订单信息。假设所有的表都有一个自增的主键”id”。

客户信息 (users)
フィールド名型概要idintプライマリキー, AUTO_INCREMENTnametext氏名gendertext性別 (male, female, other)ranktext会員ランク (general, premium)
产品信息
フィールド名型概要idintプライマリキー, AUTO_INCREMENTnametext製品名model_numbertext型番priceint価格maker_idint商品のメーカのid
制造商信息
フィールド名型概要idintプライマリキー, AUTO_INCREMENTnametext会社名
订单信息 (订单)
フィールド名型概要idintプライマリキー, AUTO_INCREMENTorder_dateint注文日時user_idint注文したユーザのID
订单信息和产品信息的连接表(order_product)

这是一个用于合并上述orders和products的表格,用于记录每个订单中所购买的商品。

フィールド名型概要idintプライマリキー, AUTO_INCREMENTorder_idint注文IDproduct_idint製品ID

模式定义

以下是将上述表结构替换为GraphQL模式定义的结果。请注意,这里只是部分内容。您可以在github.com上查看完整定义。(这些定义是从JavaScript实现中自动生成的。)

"""お客様の情報"""
type User {
  id: ID
  name: String

  """性別"""
  gender: Gender

  """会員ランク"""
  rank: MemberRank
  orders: OrderConnection
}

"""商品"""
type Product {
  id: ID

  """商品名"""
  name: String

  """型番"""
  modelNumber: String

  """販売価格"""
  price: Int
  maker: Maker
}

"""製造業者"""
type Maker {
  id: ID
  name: String
  products: ProductConnection
}

"""注文情報"""
type Order {
  id: ID

  """注文日時(UNIX TIME)"""
  orderDate: Int

  """注文したユーザ"""
  user: User

  """注文に含まれた商品"""
  products(limit: Int, offset: Int = 0): ProductConnection
}

此外,为用户的等级和性别定义了以下枚举类型。

"""性別"""
enum Gender {
  """男性"""
  male

  """女性"""
  female

  """その他"""
  other
}

"""会員ランク"""
enum MemberRank {
  """一般会員"""
  general

  """ゴールド会員"""
  gold

  """プラチナ会員"""
  platinum
}

关于 “连接”

GraphQL中有一种被称为Connection的规范。虽然听起来很复杂,但实质上就是分页功能。它可以通过数据库的limit、offset来限制查询结果数量,或者设置开始显示的位置。

另外,除了通过数字指定偏移量来确定起始位置之外,还可以通过游标来确定起始位置的方法。游标指定是指在数据库的每一行中设置主键(如哈希值),并使用该值来确定起始位置的方法。这对于处理总是不断添加新行的数据非常有效。例如,Twitter和Facebook的时间线总是在不断添加新行。由于使用偏移量来确定位置会导致行的位置不断更新,因此无法指定目标行。在这种情况下,可以使用游标以行的值本身来确定位置。

请参考以下内容,介绍了有关GraphQL连接的信息。

    • Pagination(公式サイト)

 

    GraphQL入門 – 使いたくなるGraphQL#より実践的な使い方

为了简化,我们将使用偏移量(offset)和限制(limit)来实现分页功能。
在上述模式定义中,XxxxxConnection代表分页功能。

例如,制造商在产品中定义了一个名为ProductConnection的项目。

"""製造業者"""
type Maker {
  id: ID
  name: String
  products: ProductConnection
}

以下是对此的查询:
制造商(maker)创建的产品组(products),查询从第3到第10个项的结果。
边(edges)和节点(node)是规范的术语。

query {
   makers {
      id,
      name,
      products(limit:10, offset: 3) {
          edges {
              node {
                 id,
                 name
             }
          }
      }
   }
}

实际上,如果要实现Connection,我们可以利用一个叫做graphql-relay的库提供的一些方便的函数。

实施

现在,让我来介绍一下我们已经实际实施的部分。

代码库

这次,我已经将我自己制作的内容上传到了以下的Github存储库中。如果对您有所帮助,我将不胜荣幸。

    https://github.com/hiroyky/graphql_api_sample

在中文中,你可以这样表达”express和GraphQL”:
Express和GraphQL

首先,将包含Nodejs入口点的app.ts和包含/graphql入口点的graphql.ts放在此处。

app.ts的中文释义

启动处理本身与常见的使用express框架创建的Web应用程序相同。
使用app.use(“/graphql”, “…”)设置了GraphQL的路由。
此外,还进行了数据库连接处理。MysqlDriver是一个类,用于连接MySQL等数据库。
(省略了断开连接处理的说明。)

import bodyParser from "body-parser";
import express from "express";
import http from "http";
import graphql from "./graphql";
import MysqlDriver from "./drivers/mysql-driver";

const port = process.env.PORT || 3000;
const app = express();
const mysqlDriver = new MysqlDriver();

app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

// /graphqlのパスに対してGraphQLへのルーティングを設定
app.use("/graphql", (req, res) => graphql(mysqlDriver)(req, res));

// 初期化処理完了時の処理
// HTTPリクエストを受け付けるようにする.
app.on("ready", () => {
    app.listen(port, () => {
        console.log("ready");
    });
});

http.createServer(app);

// 初期化関数が完了したらreadyイベントを発火する.
init().then(() => app.emit("ready"));

// 初期化処理
// MySQLへの接続を行っている.
async function init() {
    await mysqlDriver.open({
        host: process.env.DB_HOSTNAME,
        user: process.env.DB_USERNAME,
        database: process.env.DB_NAME,
        password: process.env.DB_PASSWORD,
        port: parseInt(process.env.DB_PORT as string),
    });
}
这个文件是graphql.ts。

express-graphql中的graphqlHTTP()函数返回为GraphQL的入口点,接受用于schema和MySQL连接的驱动类实例db作为参数。
通过在context中设置对象,可以直接将其传递给后续提到的resolve函数。
在resolve函数中,将需要使用的对象等设置在context中。


import graphqlHTTP from "express-graphql";
import MySqlDriver from "./drivers/mysql-driver";
import schema from "./schema/schema";

export default function getGraphQLHttp(db: MySqlDriver) {

    return graphqlHTTP(() => ({
        graphiql: true,
        schema,
        context: { db },
    }));
}

模式的定义 de

好的,现在让我们实现GraphQL的架构定义吧。

schema.ts的汇编。

首先,在schema.ts文件中定义了架构的框架。在这个文件中,分别设置了query(参考系)和mutation(更新系)的定义。将所有的定义都写在schema.ts文件中也没有问题,但为了可读性的考虑,我们将其分成了几个部分。在这里,我们将导出的schema设置为前进的graphqlHTTP()的schema。

import { GraphQLObjectType, GraphQLSchema } from "graphql";
import mutations from "./mutations";
import queries from "./queries";

const schema = new GraphQLSchema({
    query: new GraphQLObjectType({
        name: "GraphQLSampleQueries",
        fields: () => ({
            ...queries,
        }),
    }),

    mutation: new GraphQLObjectType({
        name: "GraphQLSampleMutations",
        fields: () => ({
            ...mutations,
        }),
    }),
});

export default schema;
查询.ts

在queries.ts中实现查询(参照系)的定义。请注意,这里只提供了用户信息(User)和产品信息(Product)的定义,并省略了其他定义。请查看Github上的代码实现其他定义。

对于用户、用户群、产品、产品群,分别设置如下内容:
– 在type中设置资源的类型定义。
– 在args中设置可以用于获取和更新资源时的参数定义。
– 在resolve中设置用于获取该资源的函数。

用户,为了准备单数和复数形式的情况,
– 在单数形式中,使用主键返回一行数据。
– 在复数形式中,返回多行匹配的结果。
此外,在复数形式中,使用前面提到的连接设置了适用于分页的类型。

import { resolveProduct, resolveProducts } from "../resolvers/product-resolver";
import { resolveUser, resolveUsers } from "../resolvers/user-resolver";
import { IdArgument, ProductArgment, UserArgument } from "./argument";
import {
    Product,
    ProductConnection,
    User,
    UserConnection,
} from "./query-type";

const userQueries = {
    user: {
        type: User,
        args: IdArgument,
        resolve: resolveUser,
    },

    users: {
        type: UserConnection,
        args: UserArgument,
        resolve: resolveUsers,
    },
};

const productQueries = {
    product: {
        type: Product,
        args: IdArgument,
        resolve: resolveProduct,
    },

    products: {
        type: ProductConnection,
        args: ProductArgment,
        resolve: resolveProducts,
    },
};

export default {
    ...userQueries,
    ...productQueries,
};

根据这个定义,例如当请求以下查询时,设置在users中的resolve函数:resolveUsers()将被执行,并且其返回值将作为API的响应。

query {
   users(gender:"female", limit:10, offset:0) {
       name
   }
}
突变.ts

在mutations.ts中实现mutation(更新类)的定义。写法与query(参考类)相同。

import { GraphQLNonNull } from "graphql";
import { createUser, updateUser } from "../resolvers/user-resolver";
import { UserInsertArgument, UserUpdateArgument } from "./argument";
import { User } from "./query-type";

const userMutations = {
    createUser: {
        type: User,
        args: {
            user: {
                type: new GraphQLNonNull(UserInsertArgument),
            },
        },
        resolve: createUser,
    },

    updateUser: {
        type: User,
        args: {
            user: {
                type: new GraphQLNonNull(UserUpdateArgument),
            },
        },
        resolve: updateUser,
    },
};

export default {
    ...userMutations,
};

通过这样做,可以执行以下查询。执行与createUser()函数关联的resolve函数,该函数将执行数据库的写入操作。并且通过返回写入后的值来创建响应。

mutation {
  createUser(
    user: {
      name:"原田",
      gender: "female"
    }
  ) {
    id, name, gender
  }
}

模式定义(类型定义)

我们先来实现通过type指定的类型定义,比如User、Product等等。这些定义需要在query-type.ts文件中进行编写。
另外,在这些定义中使用到的枚举类型可以单独分离到enum-type.ts文件中进行编写。(虽然没有必要进行分离,但目前来看这样可以提高可读性)

查询类型.ts (Zhǐ

在GraphQLObjectType中定义了类型。然后定义了以下内容。结构体的定义和所做的工作是相同的。
– name: 类型名称
– description: 类型描述(可选的)
– fields: 类型的内容

另外,在后半部分,我们定义了用于实现分页导航的Connection类型。我们可以使用graphql-relay提供的connectionDefinitions()轻松实现它。

import { GraphQLID, GraphQLInt, GraphQLObjectType, GraphQLString } from "graphql";
import { connectionDefinitions } from "graphql-relay";
import { resolveByParentMakerId } from "../resolvers/maker-resolver";
import { resolveProductsByRelatedOrder } from "../resolvers/product-resolver";
import { resolveUserByParentId } from "../resolvers/user-resolver";
import { ConnectionArgument } from "./argument";
import { GenderEnum, MemberRank } from "./enum-type";

export const User = new GraphQLObjectType({
    name: "User",
    description: "お客様の情報",
    fields: () => ({
        id: { type: GraphQLID },
        name: { type: GraphQLString },
        gender: { type: GenderEnum, description: "性別" },
        rank: { type: MemberRank, description: "会員ランク" },
        orders: { type: OrderConnection },
    }),
});

export const Product = new GraphQLObjectType({
    name: "Product",
    description: "商品",
    fields: () => ({
        id: { type: GraphQLID },
        name: { type: GraphQLString, description: "商品名" },
        modelNumber: { type: GraphQLString, description: "型番" },
        price: { type: GraphQLInt, description: "販売価格" },
        maker: {
            type: Maker,
            resolve: resolveByParentMakerId,
        },
    }),
});

export const Maker = new GraphQLObjectType({
    name: "Maker",
    description: "製造業者",
    fields: () => ({
        id: { type: GraphQLID },
        name: { type: GraphQLString },
        products: { type: ProductConnection },
    }),
});

export const Order = new GraphQLObjectType({
    name: "Order",
    description: "受注情報",
    fields: () => ({
        id: { type: GraphQLID },
        orderDate: { type: GraphQLInt, description: "受注日時(UNIX TIME)" },
        user: {
            type: User,
            description: "注文したユーザ",
            resolve: resolveUserByParentId,
        },
        products: {
            type: ProductConnection,
            args: ConnectionArgument,
            description: "注文に含まれた商品",
            resolve: resolveProductsByRelatedOrder,
        },
    }),
});

export const { connectionType: UserConnection } = connectionDefinitions({
    name: "User",
    nodeType: User,
});

export const { connectionType: ProductConnection } = connectionDefinitions({
    name: "Product",
    nodeType: Product,
});

export const { connectionType: MakerConnection } = connectionDefinitions({
    name: "Maker",
    nodeType: Maker,
});

export const { connectionType: OrderConnection } = connectionDefinitions({
    name: "Order",
    nodeType: Order,
});
枚举类型.ts

在enum-type.ts文件中定义了枚举类型。

import { GraphQLEnumType } from "graphql";

export const GenderEnum = new GraphQLEnumType({
    name: "Gender",
    description: "性別",
    values: {
        male: { value: "male", description: "男性" },
        female: { value: "female", description: "女性" },
        other: { value: "other", description: "その他" },
    },
});

export const MemberRank = new GraphQLEnumType({
    name: "MemberRank",
    description: "会員ランク",
    values: {
        general: { value: "general", description: "一般会員" },
        gold: { value: "gold", description: "ゴールド会員" },
        platinum: { value: "platinum", description: "プラチナ会員" },
    },
});

Argument的定义

让我们先来实现基于`args`参数指定的Argument的定义。即使指定了未在此处定义的值,也会被忽略。
下面的查询中,指定了`gender: “male”`的部分。

query {
    users(gender: "male") {
       id, name
    }
}
争论.ts

在argument.ts中,我们编写了Argument的类型。在这里,我们只描述了User和Product,其他内容被省略了。您可以在GitHub上的代码中查看所有的定义。

在此,根据前面所述,我们将定义的”Argument”分别设置给对应的args。
– UserArgument将设置给user和users的args。
– UserInsertArgument将设置给createUser的args。
– UserUpdateArgument将设置给updateUser的args。
– ProductArgument将设置给product和products的args。

import { GraphQLID, GraphQLInputObjectType, GraphQLInt, GraphQLNonNull, GraphQLString } from "graphql";
import { GenderEnum, MemberRank } from "./enum-type";

const RangeIntType = new GraphQLInputObjectType({
    name: "RangeInt",
    fields: () => ({
        min: { type: GraphQLInt },
        max: { type: GraphQLInt },
    }),
});

export const IdArgument = {
    id: { type: GraphQLID },
};

export const ConnectionArgument = {
    limit: { type: GraphQLInt },
    offset: { type: GraphQLInt, defaultValue: 0 },
};

export const UserArgument = {
    ...IdArgument,
    ...ConnectionArgument,
    name: { type: GraphQLString },
    gender: { type: GenderEnum },
    rank: { type: MemberRank },
};

export const UserInsertArgument = new GraphQLInputObjectType({
    name: "UserInputArgument",
    fields: () => ({
        name: { type: new GraphQLNonNull(GraphQLString) },
        gender: { type: new GraphQLNonNull(GraphQLString) },
        rank: { type: GraphQLString },
    }),
});

export const UserUpdateArgument = new GraphQLInputObjectType({
    name: "UserUpdateArgument",
    fields: () => ({
        id: { type: new GraphQLNonNull(GraphQLID) },
        name: { type: GraphQLString },
        gender: { type: GraphQLString },
        rank: { type: GraphQLString },
    }),
});

export const ProductArgment = {
    ...IdArgument,
    ...ConnectionArgument,
    name: { type: GraphQLString },
    modelNumber: { type: GraphQLString },
    price: { type: RangeIntType },
};

解决函数的实现

我将实现之前定义的模式中设置的resolve函数。这个resolve函数将与MySQL进行查询,并将其返回值作为响应。由于后端数据库是MySQL,所以以下是处理方式。

    1. 根据args的内容生成适用于MySQL的SQL语句。

 

    1. 进行MySQL查询(参考、更新)。

 

    1. 将查询结果适应GraphQL模式进行处理。

 

    作为返回值返回。

当然的是,如果后端数据存储方式不同,就需要编写相应的处理方法。无论如何,resolve函数将执行数据的获取和更新操作,并以符合模式定义的格式返回值。

resolve函数接受三个参数,如下所示。

function resolveUsers(parent: any, args: any, context: any) {
}
    • parent: 親となっているresolve関数の戻り値

 

    • args: ユーザがGraphQLの引数の値

 

    context: graphqlHTTP()のcontextで指定した値

让我们考虑一个关于产品信息查询的例子。在下面的查询中,我们以id: 312作为参数,请求相应产品的id、名称和制造公司的前置条件。

query {
    product(id: 312) {
        id,
        name,
        maker {
            name
        }
    }
}

根据前景的规划,我们设定产品如下。

product: {
    type: Product,
    args: IdArgument,
    resolve: resolveProduct,
}
const Product = new GraphQLObjectType({
    name: "Product",
    description: "商品",
    fields: () => ({
        id: { type: GraphQLID },
        name: { type: GraphQLString, description: "商品名" },
        modelNumber: { type: GraphQLString, description: "型番" },
        price: { type: GraphQLInt, description: "販売価格" },
        maker: {
            type: Maker,
            resolve: resolveMakerByParentMakerId,
        },
    }),
});

在这种情况下,将调用resolveProduct()和resolveMakerByParentMakerId()这两个函数。后者是嵌套在Product中的。另外,我已在最初的graphql.ts文件中设置了以下上下文。

graphqlHTTP(() => ({
    graphiql: true,
    schema,
    context: { db },
}));

因此,传递给resolve函数的三个参数的值分别如下。

    • resolveProduct

parent: なし
args: { id: 312 }
context: { db: db },

resolveMakerByParentMakerId

parent: resolveProductの戻り値
args: なし
context: { db: db }

在上下文中,設定的{db}將直接傳入。因此,我們應該在一開始就將共用於所有解析函數的值通過上下文傳遞。

请注意,由于需要远离GraphQL的介绍并且涉及数据库查询等内容,因此本文省略了对resolve函数的具体描述。建议您参考Github上的代码。

最终

我用Node.js实现了GraphQL的API,并将代码上传到了Github(https://github.com/hiroyky/graphql_api_sample)。

广告
将在 10 秒后关闭
bannerAds