Skip to content
Linbudu's Blog

【outdated】在MidwayJS中使用 TypeGraphQL + TypeORM

1 min read

前言

其实去年六七月左右我就有尝试过在 MidwayJS 中使用 GraphQL 及相关技术栈了了, 但是当时真的太菜了, 完全不清楚依赖注入啊容器啊相关的概念, 于是半途而废放弃了, 改成用原生的 Apollo-Server 了.

这一次是在看到了淘系技术黑皮书中 JSCON 老师(不知道花名...)的文章中给出的结合示例我才有了思路, 但是由于文中的例子可能是 Midway 1 的写法, 在 Midway 2 中部分写法已经不能使用了, 因此这里我又扩展了下, 得到了 EggJS 中间件与 Koa 中间件的两种写法, 这里做一个记录(因为真的还挺有成就感).

阅读本文前需要你了解以下技术栈:

  • GraphQL
  • Apollo-Server
  • TypeGraphQL
  • MidwayJS & EggJS & KoaJS
  • TypeORM

Koa 中间件

由于没有仔细看文档中的示例代码, 导致我额外浪费了一个小时, 淦.

首先在 src/middleware 下新建graphql.ts,

我最开始的写法是这样的:

1import * as path from "path";
2import { Provide, Config, App } from "@midwayjs/decorator";
3import {
4 IWebMiddleware,
5 IMidwayKoaContext,
6 IMidwayKoaNext,
7 IMidwayKoaApplication,
8} from "@midwayjs/koa";
9
10import { ApolloServer, ServerRegistration } from "apollo-server-koa";
11import { buildSchemaSync } from "type-graphql";
12
13type ApolloMwConfig = { [key: string]: any } & ServerRegistration;
14
15@Provide("GraphQLMiddleware")
16export class GraphqlMiddleware implements IWebMiddleware {
17 @Config("apollo")
18 config: ApolloMwConfig;
19
20 resolve() {
21 console.log("Apollo Config", this.config);
22 return async (ctx: IMidwayKoaContext, next: IMidwayKoaNext) => {
23 const server = new ApolloServer({
24 schema: buildSchemaSync({
25 resolvers: [path.resolve("./src", "resolver/*")],
26 container: ctx.requestContext,
27 }),
28 });
29 console.log("Apollo-GraphQL Invoke");
30 await next();
31 return server.getMiddleware(this.config);
32 };
33 }
34}

先不说是哪里错了, 在src/configuration.ts下注册该全局中间件:

1import { Configuration, App } from "@midwayjs/decorator";
2import { ILifeCycle } from "@midwayjs/core";
3import { IMidwayKoaApplication } from "@midwayjs/koa";
4
5@Configuration({
6 imports: ["./lib/orm"],
7 importConfigs: ["./config"],
8})
9export class ContainerConfiguration implements ILifeCycle {
10 @App()
11 app: IMidwayKoaApplication;
12
13 async onReady() {
14 console.log("onReady Hook");
15 this.app.use(await this.app.generateMiddleware("GraphQLMiddleware"));
16 }
17}

这样使用的结果是 /graphql 路径直接 404 了, 当时觉得没啥问题, 执行链路是这样的:

  • 执行 onReady, 执行this.app.generateMiddleware('GraphQLMiddleware')
  • 实例化GraphqlMiddleware, 调用 resolve 方法, resolve 内部的异步方法返回Apollo-Server使用getMiddleware方法生成的中间件, 然后整体就相当于this.app.use(ApolloMiddleware), 没毛病啊?

直到这里我还是没发现问题, 当我尝试在 resolve 方法中直接ctx.app.use(server.getMiddleware(this.config))的时候又可以了! 那么问题就很明朗了, 中间件没注册上. 再回去看文档中"第三方中间件"一节, 它的示例代码是这样的:

1import * as koaStatic from "koa-static";
2
3@Provide()
4export class ReportMiddleware implements IWebMiddleware {
5 resolve() {
6 return koaStatic(root, opts);
7 }
8}

好家伙我直接好家伙, 果然嵌套害死人. 修改 GraphQL 中间件的代码:

1import * as path from "path";
2import { Provide, Config, App } from "@midwayjs/decorator";
3import { IWebMiddleware, IMidwayKoaApplication } from "@midwayjs/koa";
4
5import { ApolloServer, ServerRegistration } from "apollo-server-koa";
6import { buildSchemaSync } from "type-graphql";
7
8// some extra config
9type ApolloMwConfig = { [key: string]: any } & ServerRegistration;
10
11@Provide("GraphQLMiddleware")
12export class GraphqlMiddleware implements IWebMiddleware {
13 @Config("apollo")
14 config: ApolloMwConfig;
15
16 @App()
17 app: IMidwayKoaApplication;
18
19 resolve() {
20 console.log("Apollo Config", this.config);
21 const server = new ApolloServer({
22 schema: buildSchemaSync({
23 resolvers: [path.resolve("./src", "resolver/*")],
24 container: this.app.getApplicationContext(),
25 }),
26 });
27 console.log("Apollo-GraphQL Invoke");
28
29 return server.getMiddleware(this.config);
30 }
31}

淦, 然后果然就可以了, 这里说明几个地方:

  • TypeGraphQL 本身不会提供容器的支持, 因此如果你想要以依赖注入的方式使用它提供的各个装饰器就要自己提供容器, 框架层面比如这里的 MidwayJS 和 NestJS(作者还手撸了个 TypeGraphQL-NestJS 的集成包), 因此这里在生成 schema 时需要传入 Midway 的运行时容器.
  • 原本我们可以通过ctx.requestContext拿到容器, 现在拿不到 ctx 参数了, 因此需要注入 app 实例然后才能获取到容器.getApplicationContext()方法是所有上层框架 app 都会实现的接口.

然后编写个简单的解析器就能打开/graphql了:

1import { Provide } from "@midwayjs/decorator";
2import { Resolver, Query } from "type-graphql";
3
4import User from "../graphql/user";
5
6@Provide()
7@Resolver((of) => User)
8export default class UserResolver {
9 constructor() {}
10
11 @Query((returns) => User)
12 async GetRandomUser(): Promise<User> {
13 return {
14 id: Math.floor(Math.random() * 100),
15 name: "林不渡",
16 };
17 }
18}

(看到GraphQL Playground我真的眼泪都要下来了)

image-20210122114614810

暂时不太清楚会不会遇到更多诡异的问题...

Egg 中间件

Egg 中间件需要遵守 EggJS 的相关规范, 有兴趣的同学可以自己去阅读 EggJS 的文档.

src/app/middleware下新建eggraphql.ts(这名字还挺好玩吧), 这里用的是函数式写法:

1import * as path from "path";
2import { IMidwayWebNext } from "@midwayjs/web";
3
4import { Context } from "egg";
5
6import { ApolloServer, ServerRegistration } from "apollo-server-koa";
7import { buildSchemaSync } from "type-graphql";
8
9export default (options: ServerRegistration) => {
10 return async function graphql(ctx: Context, next: IMidwayWebNext) {
11 await next();
12 const server = new ApolloServer({
13 schema: buildSchemaSync({
14 resolvers: [path.resolve(ctx.app.baseDir, "resolver/*.ts")],
15 container: ctx.app.applicationContext,
16 }),
17 });
18 ctx.app.use(server.getMiddleware(options));
19 };
20};

src/config/config.default.ts中配置这个中间件, 并且传入配置:

1import { ServerRegistration } from "apollo-server-koa";
2import { EggAppConfig, EggAppInfo, PowerPartial } from "egg";
3import { ConnectionOptions } from "../lib/orm";
4
5export type DefaultConfig = PowerPartial<EggAppConfig>;
6
7export default (appInfo: EggAppInfo) => {
8 const config = {} as DefaultConfig;
9
10 // eggjs版本的全局中间件还是要在这里开启
11 config.middleware = ["eggraphql"];
12
13 config["eggraphql"] = {
14 path: "/eggraphql",
15 };
16
17 config.security = {
18 csrf: false,
19 };
20
21 return config;
22};

访问/eggraphql:

image-20210122115339132

TypeORM

MidwayJS 提供了@midwayjs/orm这个包来为 TypeORM 提供集成使用, 也就是受这个影响我觉得是否也可以整个@midwayjs/type-graphql(之前还在实习的时候我有看到过内部一位大佬封装了个 faas 版本的这玩意).

源码也很简单, 大致来看一下:

  • config/ 配置
  • configuration.ts 可以理解为在这里初始化数据库连接 & 注入相关实例如 Repository 等到容器
  • index.ts @EntityModel @InjectEntityModel方法实现
  • hook.ts TypeORM 连接的生命周期钩子, 包括before/after Create/Close
  • repository.ts getRepository API

先从 index.ts 开始, 由于比较简单就不多做讲解了, 直接看注释

有部分我自己新增的代码, 详见官方仓库 midway-components

1import {
2 EntityOptions,
3 getMetadataArgsStorage,
4 ObjectType,
5 EntitySchema,
6 Repository,
7 TreeRepository,
8 MongoRepository,
9 Connection,
10 getRepository,
11} from 'typeorm';
12import { ViewEntityOptions } from 'typeorm/decorator/options/ViewEntityOptions';
13import { saveModule, attachClassMetadata } from '@midwayjs/core';
14
15// 容器中的token
16export const CONNECTION_KEY = 'orm:getConnection';
17export const MANAGER_KEY = 'orm:getManager';
18export const ENTITY_MODEL_KEY = 'entity_model_key';
19export const EVENT_SUBSCRIBER_KEY = 'event_subscriber_key';
20export const ORM_MODEL_KEY = '__orm_model_key__';
21
22export { ConnectionOptions } from 'typeorm';
23
24// @Entity的包装, 主要逻辑是将实体类注册到内部的模块关系映射中
25export function EntityModel(
26 nameOrOptions?: string | EntityOptions,
27 maybeOptions?: EntityOptions
28): ClassDecorator {
29 const options =
30 (typeof nameOrOptions === 'object'
31 ? (nameOrOptions as EntityOptions)
32 : maybeOptions) || {};
33 const name = typeof nameOrOptions === 'string' ? nameOrOptions : options.name;
34
35 return function (target) {
36 if (typeof target === 'function') {
37 saveModule(ENTITY_MODEL_KEY, target);
38 } else {
39 saveModule(ENTITY_MODEL_KEY, (target as object).constructor);
40 }
41
42 // 就是TypeORM的@Entity内部的逻辑
43 getMetadataArgsStorage().tables.push({
44 target: target,
45 name: name,
46 type: 'regular',
47 orderBy: options.orderBy ? options.orderBy : undefined,
48 engine: options.engine ? options.engine : undefined,
49 database: options.database ? options.database : undefined,
50 schema: options.schema ? options.schema : undefined,
51 synchronize: options.synchronize,
52 withoutRowid: options.withoutRowid,
53 });
54 };
55}
56
57// @EntityView 装饰器 略过
58export function EntityView(
59 nameOrOptions?: string | ViewEntityOptions,
60 maybeOptions?: ViewEntityOptions
61): ClassDecorator {
62 // ...
63}
64
65// 在类中使用@InjectEntityModel装饰器将Repository实例注入到属性
66export function InjectEntityModel(
67 modelKey?: any,
68 connectionName = 'default'
69): PropertyDecorator {
70 return (target, propertyKey) => {
71 // 将元数据添加到类上, 这里的类即使用了此装饰器的类
72 // 后续的registerDataHandler逻辑会使用此元数据来实例化Repository
73 attachClassMetadata(
74 ORM_MODEL_KEY,
75 {
76 key: {
77 modelKey,
78 connectionName,
79 },
80 propertyName: propertyKey,
81 },
82 target
83 );
84 };
85}
86
87// 注入连接
88export function InjectConnection(
89 connectionName = 'default'
90): PropertyDecorator {
91 return (target, propertyKey) => {
92 // ...
93}
94
95// 注入实体管理器
96export function InjectManager(connectionName = 'default'): PropertyDecorator {
97 return (target, propertyKey) => {
98 // ...
99}
100
101
102/**
103 * for hooks useEntityModel method
104 * @param clz
105 * @param instanceName
106 */
107export function useEntityModel<Entity>(
108 clz: ObjectType<Entity>,
109 connectionName?: string
110): Repository<Entity> {
111 return getRepository<Entity>(clz, connectionName);
112}
113
114export { OrmConfiguration as Configuration } from './configuration';

然后是 hook.ts 和 configuration.ts, 先来看看都包含了哪些 hook:

1import { saveModule } from "@midwayjs/core";
2import { Connection, ConnectionOptions } from "typeorm";
3
4export const ORM_HOOK_KEY = "__orm_hook_for_configuration__";
5
6export interface OrmConnectionHook {
7 beforeCreate?(opts?: ConnectionOptions): Promise<ConnectionOptions>;
8 afterCreate?(
9 conn?: Connection,
10 opts?: ConnectionOptions
11 ): Promise<Connection>;
12 beforeClose?(conn?: Connection, connectionName?: string): Promise<Connection>;
13 afterClose?(conn?: Connection): Promise<Connection>;
14}
15
16export function OrmHook(): ClassDecorator {
17 return function (target) {
18 if (typeof target === "function") {
19 saveModule(ORM_HOOK_KEY, target);
20 } else {
21 saveModule(ORM_HOOK_KEY, (target as object).constructor);
22 }
23 };
24}

configuration.ts 中我们要做的事情主要有:

  • 获取并格式化 ormconfig, 创建连接(组)
  • 注册连接
  • ...没了
1import { ILifeCycle, IMidwayContainer } from "@midwayjs/core";
2import { Configuration, listModule, Config } from "@midwayjs/decorator";
3import {
4 createConnection,
5 getConnection,
6 getRepository,
7 getManager,
8 ConnectionOptions,
9 Connection,
10} from "typeorm";
11import {
12 ENTITY_MODEL_KEY,
13 EVENT_SUBSCRIBER_KEY,
14 CONNECTION_KEY,
15 ORM_MODEL_KEY,
16 MANAGER_KEY,
17} from ".";
18import { ORM_HOOK_KEY, OrmConnectionHook } from "./hook";
19import { join } from "path";
20
21// 导入config.orm字段
22@Configuration({
23 importConfigs: [join(__dirname, "./config")],
24 namespace: "orm",
25})
26export class OrmConfiguration implements ILifeCycle {
27 @Config("orm")
28 private ormConfig: any;
29
30 private connectionNames: string[] = [];
31
32 async onReady(container: IMidwayContainer) {
33 // 注册后才能在类中注入Repository, Connection EntityManager同理
34 (container as any).registerDataHandler(
35 ORM_MODEL_KEY,
36 (key: { modelKey: any; connectionName: string }) => {
37 const repo = getRepository(key.modelKey, key.connectionName);
38 return repo;
39 }
40 );
41
42 // 在@EntityModel中调用了saveModule保存实体, 在这里就可以获取到了
43 const entities = listModule(ENTITY_MODEL_KEY);
44 const eventSubs = listModule(EVENT_SUBSCRIBER_KEY);
45
46 const opts = this.formatConfig();
47
48 for (const connectionOption of opts) {
49 connectionOption.entities = entities || [];
50 connectionOption.subscribers = eventSubs || [];
51 const name = connectionOption.name || "default";
52 this.connectionNames.push(name);
53 let isConnected = false;
54 // 尝试建立连接
55 try {
56 const conn = getConnection(name);
57 if (conn.isConnected) {
58 isConnected = true;
59 }
60 } catch {}
61 if (!isConnected) {
62 const rtOpt = await this.beforeCreate(container, connectionOption);
63 const con = await createConnection(rtOpt);
64 await this.afterCreate(container, rtOpt, con);
65 }
66 }
67
68 // 在容器中注册连接
69 container.registerObject(CONNECTION_KEY, (instanceName) => {
70 if (!instanceName) {
71 instanceName = "default";
72 }
73 return getConnection(instanceName);
74 });
75 }
76
77 async onStop(container: IMidwayContainer) {
78 await Promise.all(
79 Object.values(this.connectionNames).map(async (connectionName) => {
80 const conn = getConnection(connectionName);
81
82 await this.beforeClose(container, conn, connectionName);
83
84 if (conn.isConnected) {
85 await conn.close();
86 }
87
88 await this.afterClose(container, conn);
89 })
90 );
91
92 this.connectionNames.length = 0;
93 }
94
95 formatConfig(): any[] {
96 const originConfig = this.ormConfig;
97 if (originConfig?.type) {
98 originConfig.name = "default";
99 return [originConfig];
100 } else {
101 const newArr = [];
102
103 for (const [key, value] of Object.entries(originConfig)) {
104 (value as any).name = key;
105 newArr.push(value);
106 }
107
108 return newArr;
109 }
110 }
111
112 private async beforeCreate(
113 container: IMidwayContainer,
114 opts: ConnectionOptions
115 ): Promise<ConnectionOptions> {
116 let rt = opts;
117 const clzzs = listModule(ORM_HOOK_KEY);
118 for (const clzz of clzzs) {
119 const inst: OrmConnectionHook = await container.getAsync(clzz);
120 if (inst.beforeCreate && typeof inst.beforeCreate === "function") {
121 rt = await inst.beforeCreate(rt);
122 }
123 }
124 return rt;
125 }
126
127 private async afterCreate(
128 container: IMidwayContainer,
129 opts: ConnectionOptions,
130 con: Connection
131 ): Promise<Connection> {
132 let rtCon: Connection = con;
133 const clzzs = listModule(ORM_HOOK_KEY);
134 for (const clzz of clzzs) {
135 const inst: OrmConnectionHook = await container.getAsync(clzz);
136 if (inst.afterCreate && typeof inst.afterCreate === "function") {
137 rtCon = await inst.afterCreate(con, opts);
138 }
139 }
140 return rtCon;
141 }
142
143 private async beforeClose(
144 container: IMidwayContainer,
145 con: Connection,
146 connectionName: string
147 ) {
148 let rt = con;
149 const clzzs = listModule(ORM_HOOK_KEY);
150 for (const clzz of clzzs) {
151 const inst: OrmConnectionHook = await container.getAsync(clzz);
152 if (inst.beforeClose && typeof inst.beforeClose === "function") {
153 rt = await inst.beforeClose(rt, connectionName);
154 }
155 }
156 return rt;
157 }
158
159 private async afterClose(container: IMidwayContainer, con: Connection) {
160 let rt = con;
161 const clzzs = listModule(ORM_HOOK_KEY);
162 for (const clzz of clzzs) {
163 const inst: OrmConnectionHook = await container.getAsync(clzz);
164 if (inst.afterClose && typeof inst.afterClose === "function") {
165 rt = await inst.afterClose(rt);
166 }
167 }
168 return rt;
169 }
170}

大致逻辑即是如此.

具体使用直接参考我给@midwayjs/orm提的这个文档 PR