typenexus

TypeNexus is a great tool for API encapsulation and management. It offers a clean and lightweight way to bundle TypeORM + Expressjs functionality, helping you to build applications faster while reducing template code redundancy and type conversion work.

MIT License

Downloads
353
Stars
5
Committers
3

TypeNexus

TypeNexus 是一个优秀的 API 封装和管理工具。它提供了一种清晰、轻量级的方式来捆绑 TypeORM + Express.js 功能,帮助您更快地构建应用程序,同时减少模板代码冗余和类型转换工作。

安装

$ npm install typenexus

在您的项目的 tsconfig.json 文件中设置以下选项很重要:

{
  "emitDecoratorMetadata": true,
  "experimentalDecorators": true
}

在您的项目的 package.json 文件中设置以下选项很重要:

{
  "type": "module"
}

快速开始

import { TypeNexus } from 'typenexus';

(async () => {
  const app = new TypeNexus();
  await app.start();
  // 在浏览器中打开 http://localhost:3000
})();

❶ 创建实体

实体是一个映射到数据库表(或在使用 Postgres 时映射到集合)的类。您可以通过定义一个新的类并用 @Entity() 标记来创建实体:

./src/user.entity.ts

import { Entity, PrimaryGeneratedColumn, Column, DeleteDateColumn, CreateDateColumn } from 'typenexus';
// 或者:
import { Entity, PrimaryGeneratedColumn, Column, DeleteDateColumn, CreateDateColumn } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ unique: true })
  username: string;

  @Column({ select: false })
  password: string;

  @CreateDateColumn()
  createAt: Date;
}

❷ 创建 API

./src/user.controller.ts

import { TypeNexus, Controller, Param, Body, DataSource } from 'typenexus';
import { Get, Post, Put, Delete, Patch, Delete, Head } from 'typenexus';
import { User } from './user.entity.js';

@Controller('/api/users')
export class UserController {
  constructor(@DSource() private dataSource: DataSource) {}
  @Get()          // => GET /api/users
  public async getAll(): Promise<User[]> {
    return this.dataSource.manager.find(User);
  }
  @Get('/:id')    // => GET /api/users/:id
  public async getById(@Param('id') id: string): Promise<User> {
    return this.dataSource.manager.findOne(User, id);
  }
  @Post('/:id')   // => POST /api/users/:id
  public async modify(@Body() body: { name: string; }): Promise<{ name: string; }> {
    return { name: body.name + '~~' }
  }
  @Put('/:id')    // => PUT /api/users/:id
  public async modify(@Param('id') id: string): Promise<{ uid: string; }> {
    return { uid: id }
  }
  @Delete('/:id') // => DELETE /api/users/:id
  public async modify(@Param('id') id: string): Promise<{ uid: string; }> {
    return { uid: id }
  }
  @Patch('/:id')  // => PATCH /api/users/:id
  public async patch(): Promise<any> {
    return { id: 12 }
  }
  @Head('/:id')   // => HEAD /api/users/:id
  public async head(): Promise<{ id: number; }> {
    return { id: 12 }
  }
}

这个类将在您的服务器框架 Express.js 中注册在方法装饰器中指定的路由。

❸ 创建服务器

./src/index.ts

import { TypeNexus } from 'typenexus';
import { UserController } from './user.controller.js';

;(async () => {
  const app = new TypeNexus();
  // ❶ 执行与数据库的连接。
  await app.connect({ 
    type: 'postgres',
    host: process.env.HOST || 'localhost',
    port: 5432,
    username: process.env.DB_USER || 'postgres',
    password: process.env.DB_PASS || 'wcjiang',
    database: process.env.DB_NAME || 'typenexus-base',
    synchronize: true,
    logging: true,
    entities: ['dist/entity/*.js'],
    // 或者:
    // entities: [User],      
  });
  // ❷ 🚨 请务必在 `app.connect()` 后使用它。
  app.controllers([UserController]);
  // ❸ 监听连接。
  await app.start();

})();

在浏览器中打开 http://localhost:3000/users。你会看到此操作会返回所有用户。如果你打开 http://localhost:3000/api/users/1,你会看到此操作返回用户数据。

└── src
    ├── user.controller.ts
    ├── user.entity.ts
    └── index.ts

什么是 DataSource

只有在设置了 DataSource 后,您才能与数据库进行交互。TypeORMDataSource 包含了您的数据库连接设置,并根据您使用的 RDBMS 建立初始数据库连接或连接池。

import { TypeNexus } from 'typenexus';
import crypto from 'crypto';
import User from './entity/User.js'

const app = new TypeNexus(3000, { .... });
await app.connect();

// 您可以在此处使用 DataSource 示例。
// 🚨 请务必在 `app.connect()` 后使用它。
const repos = app.dataSource.getRepository(User);
// 检查是否存在管理员账户。
const adminUser = await repos.findOneBy({ username: 'wcj' });
if (!adminUser) {
  const hashPassword = crypto.createHmac('sha256', '1234').digest('hex');
  // 创建一个管理员账户。
  const user = await repos.create({
    username: 'wcj',
    name: '管理员',
    password: hashPassword,
  });
  await repos.save(user);
}

// 🚨 请务必在 `app.connect()` 后使用它。
app.controllers([UserController]);
await app.start();

使用 app.dataSource 来获取 DataSource 实例。

什么是 DataSourceOptions

dataSourceOptions 是在创建新的 DataSource 实例时传递的数据源配置。不同的 RDBMS 有它们自己特定的选项。

import { TypeNexus, TypeNexusOptions } from 'typenexus';
const options: TypeNexusOptions = {
  dataSourceOptions: {
    type: 'postgres',
    host: process.env.POSTGRES_HOST || 'localhost',
    port: 5432,
    username: process.env.POSTGRES_USER || 'postgres',
    password: process.env.POSTGRES_PASSWORD || 'wcjiang',
    database: process.env.POSTGRES_DB || 'typenexus-base',
    synchronize: true,
    logging: true,
    entities: ['dist/entity/*.js'],
    // 或者:
    // entities: [User], 
  },
}

;(async () => {
  const app = new TypeNexus(3000, options);
  await app.connect();
  app.controllers([UserController]);
  app.express.disable('x-powered-by');
  await app.start();
})();

也可以在 app.connect() 方法内作为参数传递:

await app.connect({ ... });

什么是 Entity?

Entity 是一个映射到数据库表(或在使用 Postgres 时映射到集合)的类。您可以通过定义一个新的类并使用 @Entity() 标记来创建一个实体:

import { Entity, PrimaryGeneratedColumn, Column } from "typenexus"

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number

  @Column()
  firstName: string

  @Column()
  lastName: string

  @Column()
  isActive: boolean
}

这将创建以下数据库表:

+-------------+--------------+----------------------------+
|                          user                           |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| firstName   | varchar(255) |                            |
| lastName    | varchar(255) |                            |
| isActive    | boolean      |                            |
+-------------+--------------+----------------------------+

TypeNexus 选项

import { DataSourceOptions } from 'typeorm';
import { OptionsUrlencoded, OptionsJson, OptionsText, Options } from 'body-parser';
import { SessionOptions } from "express-session";

export interface TypeNexusOptions {
  port?: number;
  /** 全局路由前缀,例如 '/api'。 */
  routePrefix?: string;
  /** DataSourceOptions 是用于特定 DataSource 的设置和选项的接口。 */
  dataSourceOptions?: DataSourceOptions;
  /** 创建一个会话中间件 */
  session?: SessionResult | SessionCallback;
  /**
   * 指示是否启用了默认的 TypeNexus 错误处理程序。
   * 默认情况下启用。
   */
  defaultErrorHandler?: boolean;
  /**
   * 指示 TypeNexus 是否应该运行在开发模式中。
   */
  developmentMode?: boolean;
  /** Node.js 的 body 解析中间件。 */
  bodyParser?: {
    /**
     * 返回解析所有请求主体为字符串的中间件,仅查看 Content-Type 标头与类型选项匹配的请求。
     */
    text?: OptionsText;
    /**
     * 返回将所有主体解析为缓冲区的中间件,仅查看 Content-Type 标头与类型选项匹配的请求。
     */
    raw?: Options;
    /**
     * 返回仅解析 json 的中间件,仅查看 Content-Type 标头与类型选项匹配的请求。
     */
    json?: false | OptionsJson;
    /**
     * 返回仅解析 urlencoded 格式的请求主体的中间件,并且仅查看 Content-Type 标头与类型选项匹配的请求。
     * 用于解析 application/x-www-form-urlencoded 格式的请求主体。
     * @default `{extended:false}`
     */
    urlencoded?: false | OptionsUrlencoded;
  };
  /**
   * 指示是否启用了跨域资源共享。
   * 这需要安装额外的模块(express 的 cors)。
   */
  cors?: boolean | CorsOptions;
  /** Node.js 的压缩中间件。支持以下压缩编码:deflate | gzip */
  compression?: false | CompressionOptions;
  /** 默认设置 */
  defaults?: {
    /**
     * 如果设置,所有空响应将默认返回指定的状态码
     */
    nullResultCode?: number;
    /**
     * 如果设置,所有未定义的响应将默认返回指定的状态码
     */
    undefinedResultCode?: number;
  };
  /**
   * 用于每个请求检查用户授权角色的特殊函数。
   * 必须返回 true 或解析为 boolean true 的 promise 才能授权成功。
   */
  authorizationChecker?: (action: Action, roles: any[]) => Promise<boolean> | boolean;
  /**
   * 用于获取当前已授权用户的特殊函数。
   */
  currentUserChecker?: (action: Action) => Promise<any> | any;
}

参数配置示例:

new TypeNexus(3000, { routePrefix: 'api' });

更多示例

使用请求和响应对象

@Req() 装饰器注入了一个 Request 对象,而 @Res() 装饰器注入了一个 Response 对象。如果你安装了类型定义,你可以使用它们的类型:

import { Controller, Req, Res, Get } from 'typeorm';
import { Response, Request } from 'express';

@Controller()
export class UserController {
  @Get('/users') // => GET /users
  getAllUsers(@Req() request: Request, @Res() response: Response) {
    return response.send('Hello response!');
  }

  @Get('/posts') // => GET /posts
  getAllPosts(@Req() request: Request, @Res() response: Response) {
    // 一些响应函数不会返回响应对象,
    // 因此需要明确地返回它
    response.redirect('/users');
    return response;
  }
}

你可以直接使用框架的请求和响应对象。如果你想自己处理响应,只需确保从动作中返回响应对象本身。

为所有控制器路由添加前缀

如果你想要给所有路由添加前缀,例如 /api,你可以使用 routePrefix 选项:

import { TypeNexus } from 'typenexus';
import { UserController } from './controller/User.js';

;(async () => {
  const app = new TypeNexus(3033);
  // 🚨 请确保将其放在 `app.controllers()` 之前
  app.routePrefix = '/api'
  app.controllers([UserController]);
})();

你也可以通过在实例化 TypeNexus 时的参数中配置 routePrefix 来实现相同的效果:

const app = new TypeNexus(3033, {
  routePrefix: '/api'
});

为控制器添加基本路由前缀

你可以为所有特定控制器的动作添加基本路由前缀:

import { Controller, Get } from 'typeorm';

@Controller('/api')
export class UserController {
  @Get("/users/:id")  // => GET /api/users/12
  public async getOne() {}
  @Get("/users")      // => GET /api/users
  public async getUsers() {}
  // ...
}

使用 DataSource 对象

@DSource() 装饰器注入了一个 DataSource 对象。

支持构造函数中的 @DSource() 装饰器

import { Controller, Get, DSource, DataSource } from 'typenexus';
import { Response, Request } from 'express';
import { User } from '../entity/User.js';

@Controller('/users')
export class UserController {
  constructor(@DSource() private dataSource: DataSource) {}
  @ContentType('application/json')
  @Get() // => GET /users
  public async getUsers(): Promise<User[]> {
    return this.dataSource.manager.find(User);
  }
}

支持参数中的 @DSource() 装饰器

import { Controller, Get, DSource, DataSource } from 'typenexus';
import { Response, Request } from 'express';
import { User } from '../entity/User.js';

@Controller('/users')
export class UserController {
  @Get() // => GET /users
  public async getUsers(@DSource() dataSource: DataSource): Promise<User[]> {
    return dataSource.manager.find(User);
  }
}

注入请求体

要注入请求体,使用 @Body 装饰器:

import { Controller, Post, Body } from 'typeorm';

type UserBody = { username: string; id: number; };

@Controller()
export class UserController {
  @Post("/users") // => POST /users
  saveUser(@Body() user: UserBody) {
    // ...
  }
}

注入请求体参数

要注入请求体参数,使用 @BodyParam 装饰器:

import { Controller, Post, BodyParam } from 'typeorm';

type UserBody = { username: string; id: number; };

@Controller()
export class UserController {
  @Post("/users") // => POST /users
  saveUser(@BodyParam("name") userName: string) {
    // ...
  }
}

注入请求头参数

要注入请求头参数,使用 @HeaderParam 装饰器:

import { Controller, Post, HeaderParam } from 'typeorm';

@Controller()
export class UserController {
  @Post("/users")
  saveUser(@HeaderParam("authorization") token: string) {
    // ...
  }
}

如果你想要注入所有的请求头参数,使用 @HeaderParams() 装饰器。

注入查询参数

要注入查询参数,使用 @QueryParam 装饰器:

import { Controller, Get, QueryParam } from 'typeorm';

type UserBody = { username: string; id: number; };

@Controller()
export class UserController {
  @Get("/users")
  public async getUsers(@QueryParam("limit") limit: number) {
    // ....
  }
}

如果你想要注入所有的查询参数,使用 @QueryParams() 装饰器。

import { Controller, Get, QueryParams } from 'typeorm';

@Controller()
export class UserController {
  @Get("/users")
  public async getUsers(@QueryParams() query: any) {
    // ....
  }
}

注入路由参数

你可以使用 @Param 装饰器在你的控制器动作中注入参数:

import { Controller, Get, Param } from 'typeorm';

@Controller()
export class UserController {
  @Get("/users/:id")
  getOne(@Param("id") id: string) {}
}

如果你想要注入所有的参数,使用 @Params() 装饰器。

注入 Cookie 参数

要获取一个 Cookie 参数,使用 @CookieParam 装饰器:

import { Controller, Get, CookieParam, CookieParams } from 'typeorm';

@Controller()
export class UserController {
  @Get("/users")
  public async getUsers(@CookieParam("token") token: string) {
    // ....
  }
}

如果你想要注入所有的 Cookie 参数,使用 @CookieParams() 装饰器。

注入会话对象

要注入会话值,使用 @SessionParam 装饰器:

@Get("/login")
savePost(@SessionParam("user") user: User, @Body() post: Post) {}

如果你想要注入主会话对象,使用 @Session() 而不带任何参数。

@Get("/login")
savePost(@Session() session: any, @Body() post: Post) {}

Express 使用 express-session 来处理会话,所以你首先必须手动安装它才能使用 @Session 装饰器。以下是配置 Session 的示例,你还需要为 Session 创建一个数据库表实体:

import { TypeNexus, TypeNexusOptions } from 'typenexus';
import { UserController } from './controller/User.js';
import { Session } from './entity/Session.js';

const options: TypeNexusOptions = {
  // ...
  dataSourceOptions: { ... },
  session: {
    secret: 'secret',
    resave: false,
    saveUninitialized: false,
    repositoryTarget: Session,
    typeormStore: {
      cleanupLimit: 2,
      // limitSubquery: false, // 如果使用 MariaDB。
      ttl: 86400,
    }
  }
}

;(async () => {
  const app = new TypeNexus(3001, options);
  // ❶ 执行数据库连接。
  await app.connect();
  // 或者:
  // await app.connect(options.dataSourceOptions);

  // ❷ 🚨 请务必在 `app.connect()` 后使用。
  app.controllers([UserController]);
  // ❸ 监听连接。
  await app.start();

})();

以下是 Session 的数据库表实体示例:

// ./entity/Session.js
import { Column, Entity, Index, PrimaryColumn, DeleteDateColumn } from 'typeorm';
import { ISession } from 'connect-typeorm';

@Entity()
export class Session implements ISession {
  @Index()
  @Column('bigint', { transformer: { from: Number, to: Number } })
  public expiredAt = Date.now();

  @PrimaryColumn('varchar', { length: 255 })
  public id = '';

  @DeleteDateColumn()
  public destroyedAt?: Date;

  @Column('text')
  public json = '';
}

注入上传的文件

要注入上传的文件,使用 @UploadedFile 装饰器:

@Post("/file")
saveFile(@UploadedFile("fileName") file: Express.Multer.File) {}

要注入上传的多个文件,使用 @UploadedFiles 装饰器:

@Post("/files")
saveFiles(@UploadedFiles("fileName") file: Express.Multer.File[]) {}

你还可以以这种方式指定上传选项给 multer

import type { Options } from 'multer';
// 为了保持代码的整洁,最好将此函数提取到单独的文件中
const fileUploadOptions: () => Options = () => ({
  storage: multerFn.diskStorage({
    destination: (req, file, cb) => {
      //...
    },
    filename: (req, file, cb) => {
      //...
    }
  }),
  fileFilter: (req, file, cb) => {
    //...
  },
  limits: {
    fieldNameSize: 255,
    fileSize: 1024 * 1024 * 2
  }
});

// 使用选项的方式:
@Post("/file")
saveFiles(@UploadedFile("fileName", fileUploadOptions) file: Express.Multer.File) {}

要注入所有上传的文件,请改用 @UploadedFiles 装饰器。typenexus 使用 multer 来处理文件上传。

设置位置

你可以为任何动作设置 Location 头:

import { Controller, Get, Location } from 'typenexus';

@Controller()
export class UserController {
  @Get('/users')
  @Location("https://bing.com")
  public async detail() {}
}

将响应的 Location HTTP 头设置为指定的路径参数。

设置重定向

你可以为任何动作设置 Redirect 头:

import { Controller, Get, Redirect } from 'typenexus';

@Controller()
export class UserController {
  @Get('/users')
  @Redirect("http://github.com")
  public async detail() {}
}

你可以通过返回一个字符串值来覆盖 Redirect 头:

import { Controller, Get, Redirect } from 'typenexus';

@Controller()
export class UserController {
  @Get('/users')
  @Redirect("http://github.com")
  public async detail() {
    return "https://bing.com";
  }
}

你可以使用模板来生成 Redirect 头:

import { Controller, Get, Redirect } from 'typenexus';

@Controller()
export class UserController {
  @Get('/users')
  @Redirect("http://github.com/:owner/:repo")
  public async detail() { 
    return { owner: "jaywcjlove", repo: "typenexus" };
  }
}

设置自定义 HTTP 状态码

你可以为任何动作显式地设置返回的 HTTP 状态码:

import { Controller, Post, HttpCode } from 'typenexus';

@Controller()
export class UserController {
  @Post('/users')
  @HttpCode(201)
  public async saveUser() {}
}

控制空响应

如果你的控制器返回 voidPromise<void> 或 undefined,它将抛出 404 错误。如果你想要阻止这种情况,你需要使用 @OnUndefined 装饰器来指定你想要返回的状态码。

import { Controller, Param, Delete, OnUndefined, DSource, DataSource } from 'typeorm';
import { User } from '../entity/User.js';

@Controller()
export class UserController {
  constructor(@DSource() private dataSource: DataSource) {}
  @Delete("/users/:id")
  @OnUndefined(204)
  async remove(@Param("id") id: string): Promise<void> {
    return this.dataSource.manager.findOneBy(User, { id });
  }
}

@OnUndefined 在你返回可能为 undefined 或不为 undefined 的对象时也很有用。在这个例子中,findOneBy 如果找不到具有给定 id 的用户,则返回 undefined。此操作将在未找到用户时返回 404,如果找到用户则返回常规的 200

import { Controller, Param, Delete, OnUndefined, DSource, DataSource } from 'typeorm';
import { User } from '../entity/User.js';

@Controller()
export class UserController {
  constructor(@DSource() private dataSource: DataSource) {}
  @Delete("/users/:id")
  @OnUndefined(404)
  async remove(@Param("id") id: string): Promise<void> {
    return this.dataSource.manager.findOneBy(User, { id });
  }
}

你还可以指定当返回 undefined 时要使用的错误类:

import { HttpError } from 'typeorm';

export class UserNotFoundError extends HttpError {
  constructor() {
    super(404, 'User not found!');
  }
}
import { Controller, Param, Delete, OnUndefined, DSource, DataSource } from 'typeorm';
import { User } from '../entity/User.js';

@Controller()
export class UserController {
  constructor(@DSource() private dataSource: DataSource) {}
  @Get("/users/:id")
  @OnUndefined(UserNotFoundError)
  async remove(@Param("id") id: string): Promise<void> {
    return this.dataSource.manager.findOneBy(User, { id });
  }
}

如果控制器动作返回 null,则可以使用 @OnNull 装饰器。

import { Controller, Get, OnNull, Param } from 'typeorm';

@Controller()
export class UserController {
  @Get('/questions/:id')
  @OnNull(404)
  public async detail(@Param('id') id: string): Promise<string> {
    return new Promise((ok, fail) => {
      ok(null);
    });
  }
}

设置自定义头部

你可以在响应中设置任何自定义头部:

import { Controller, Get, Header, Param } from 'typeorm';

@Controller()
export class UserController {
  @Get("/users/:id")
  @Header("Cache-Control", "none")
  public async getOne(@Param('id') id: string): Promise<string> {
    // ...
  }
}

渲染模板

如果你正在使用服务器端渲染,你可以**render**任何模板:

import { Controller, Get, Render } from 'typenexus';

@Controller('/')
export class UserController {
  @Get()
  @Render("index")
  getOne() {
    return {
      title: "这些参数被使用"
    };
  }
}

要使用渲染功能,请确保正确配置了 express。要在 express 中使用渲染功能,你需要使用第三方渲染中间件,例如 ejs

$ npm install ejs

模板文件所在的目录。例如:app.set('views', './views')。默认情况下,这将设置为应用程序根目录中的 views 目录。

app.express.set('views', path.join(__dirname, 'views'));

要使用的模板引擎。例如,要使用 ejs 模板引擎:app.set('view engine', 'ejs')

app.express.set('view engine', 'ejs');

在 views 目录下创建一个名为 index.ejsejs 模板文件,内容如下:

<!DOCTYPE html>
<html>
  <head>
    <title><%= title %></title>
  </head>
  <body>
    <h1><%= title %></h1>
    <p>Welcome to <%= title %></p>
  </body>
</html>

完整的入口示例:

import { TypeNexus } from 'typenexus';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { UserController, CustomErrorHandler } from './UserController.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

;(async () => {
  const app = new TypeNexus(3002, {
    defaultErrorHandler: false,
  });
  app.express.set('views', path.join(__dirname, 'views'));
  app.express.set('view engine', 'ejs');
  app.controllers([UserController], [CustomErrorHandler]);
  await app.start();
})();

抛出 HTTP 错误

如果你想返回具有特定错误代码的错误,有一个简单的方法:

import { Controller, Get, Header, Param } from 'typeorm';

@Controller()
export class UserController {
  @Get("/users/:id")
  public async getOne(@Param('id') id: string): Promise<string> {
    const user = await dataSource.manager.findOneBy(User, { id });
    if (!user) {
      throw new NotFoundError(`未找到用户。`); // 消息是可选的
    }
    return user;
  }
}

现在,当找不到请求的 id 对应的用户时,响应将是带有 404 的 HTTP 状态码,并具有以下内容:

{
  "name": "NotFoundError",
  "message": "未找到用户。"
}

你可以使用一组准备好的错误:

  • HttpError
  • BadRequestError
  • ForbiddenError
  • InternalServerError
  • MethodNotAllowedError
  • NotAcceptableError
  • NotFoundError
  • UnauthorizedError

你还可以通过扩展 HttpError 类来创建和使用自己的错误。要定义返回给客户端的数据,你可以在错误中定义一个 toJSON 方法。

class DbError extends HttpError {
  public operationName: string;
  public args: any[];

  constructor(operationName: string, args: any[] = []) {
    super(500);
    Object.setPrototypeOf(this, DbError.prototype);
    this.operationName = operationName;
    this.args = args; // 可用于内部日志记录
  }

  toJSON() {
    return {
      status: this.httpCode,
      failedOperation: this.operationName,
    };
  }
}

启用 CORS

由于 CORS 是几乎在任何 Web API 应用程序中都会使用的功能,你可以在 typenexus 选项中启用它。

import { TypeNexus, Action } from 'typenexus';
import { UserController } from './UserController.js';

;(async () => {
  const app = new TypeNexus(3002, {
    cors: true,
  });

  app.controllers([UserController]);
  await app.start();

})();

你也可以配置 cors

import { TypeNexus, Action } from 'typenexus';
import { UserController } from './UserController.js';

;(async () => {
  const app = new TypeNexus(3002, {
    cors: {
      // cors 文档中的选项
    },
  });

  app.controllers([UserController]);
  await app.start();

})();

使用 authorization 功能

TypeNexus 提供了两个装饰器来帮助你组织应用程序中的授权。

@Authorized 装饰器

要使 @Authorized 装饰器起作用,你需要设置特殊的 TypeNexus 选项:

const app = new TypeNexus(3002, { ... });
await app.connect();

app.authorizationChecker = async (action: Action, roles: string[]) => {
  // 在这里你可以使用来自 action 的请求/响应对象
  // 如果装饰器定义了角色,它需要访问 action
  // 你可以使用它们来提供细粒度的访问检查
  // 检查器必须返回布尔值(true 或 false)
  // 或者解析为布尔值的 promise
  // 演示代码:
  const token = action.request.query.token || action.request.body.token || (action.request.headers.authorization || '').replace(/^token\s/, '');
  if (action.request.session.token !== token) return false;
  const dataSource = action.dataSource;
  const user = await dataSource.manager.findOne(User, {
    where: { username },
    select: ['username', 'id', 'roles'],
  });
  if (user && roles.find(role => user.roles.indexOf(role) !== -1)) return true;
  // @ts-ignore
  if (action.request.session.token === token) return true;
  return false;
}

app.controllers([UserController]);
await app.start();

你可以在控制器动作上使用 @Authorized

import { Controller, Authorized, Req, Res, Get } from 'typeorm';
import { Response, Request }from 'express';

@Controller()
export class UserController {
  @Authorized('POST_MODERATOR') // 你可以指定角色或角色数组
  @Post('/posts') // => POST /posts
  create(@Body() post: Post, @Req() request: Request, @Res() response: Response) {
    // ...
  }
}

@CurrentUser 装饰器

要使 @CurrentUser 装饰器起作用,你需要设置特殊的 TypeNexus 选项:

import { TypeNexus, Action } from 'typenexus';
import { UserController } from './UserController.js';
import { User } from './User.js';

;(async () => {
  const app = new TypeNexus(3002, {
    routePrefix: '/api',
    developmentMode: false,
  });

  app.currentUserChecker = async (action: Action) => {
    return new User(1, 'Johny', 'Cage');
  }

  app.controllers([UserController]);
  await app.start();
})();

你可以在控制器动作上使用 @CurrentUser

import { Controller, CurrentUser, Get } from 'typenexus';
import { User } from './User.js';

@Controller('/questions')
export class UserController {
  @Get()
  public async all(@CurrentUser() user?: User): Promise<any> {
    return {
      id: 1,
      title: 'Question by ' + user.firstName,
    };
  }
}

如果你将 @CurrentUser 标记为 required,并且 currentUserChecker 逻辑返回空结果,那么 TypeNexus 将抛出授权必需错误。

使用中间件

您可以使用任何现有的 express 中间件,或者创建您自己的中间件。要创建自己的中间件,可以使用 @Middleware 装饰器,要使用已存在的中间件,可以使用 @UseBefore@UseAfter 装饰器。

使用现有中间件

有多种方法可以使用中间件。例如,让我们尝试使用 compression 中间件:

  1. 安装 compression 中间件:
$ npm install compression
  1. 对于每个动作使用中间件:
import { Controller, Get, UseBefore } from "typeorm";
import compression from 'compression';

@Controller()
export class UserController {
  @Get('/users/:id')
  @UseBefore(compression())
  async getOne(@Param("id") id: string): Promise<any> {
      // ...
  }
}

这样,compression 中间件仅会应用于 getOne 控制器动作,并且会在动作执行之前执行。要在动作之后执行中间件,请改用 @UseAfter 装饰器。

  1. 对于每个控制器使用中间件:
import { Controller, UseBefore } from "typeorm";
import compression from 'compression';

@Controller()
@UseBefore(compression())
export class UserController { }

这样,compression 中间件将应用于 UserController 控制器的所有动作,并在其动作执行之前执行。您也可以在这里使用 @UseAfter 装饰器。

  1. 如果您希望为所有控制器全局使用 compression 模块,您可以在启动时简单地注册它:
import { TypeNexus, Action } from 'typenexus';
import { UserController } from './UserController.js';

;(async () => {
  const app = new TypeNexus(3002, {
    routePrefix: '/api',
    developmentMode: false,
  });
  app.controllers([UserController]);
  app.express.use(compression());
  await app.start();
})();

或者,您可以创建一个自定义的全局中间件,然后简单地将其执行委托给 compression 模块。

创建您自己的 express 中间件

下面是创建 express.js 中间件的示例:

  1. 有两种创建中间件的方式:

首先,您可以创建一个简单的中间件函数:

import { Request, Response, NextFunction } from 'express';

export function loggingMiddleware(request: Request, response: Response, next?: NextFunction): any {
  console.log('do something...');
  next();
}

其次,您可以创建一个类:

import { ExpressMiddlewareInterface } from 'typenexus';

export class MyMiddleware implements ExpressMiddlewareInterface {
  // 接口实现是可选的
  use(request: Request, response: Response, next?: NextFunction): any {
    console.log('do something...');
    next();
  }
}
  1. 然后,您可以这样使用它们:
import { Controller, UseBefore, UseAfter } from 'typeorm';
import { MyMiddleware, MyMiddleware2 } from './MyMiddleware';
import { loggingMiddleware } from './loggingMiddleware';

@Controller()
@UseBefore(MyMiddleware, MyMiddleware2)
@UseAfter(loggingMiddleware)
export class UserController {}
  1. 或者对每个操作使用:
import { Controller, UseBefore, UseAfter, Get } from 'typeorm';
import { MyMiddleware } from './MyMiddleware';
import { loggingMiddleware } from './loggingMiddleware';

@Controller()
export class UserController {
  @Get("/users/:id")
  @UseBefore(MyMiddleware)
  @UseAfter(loggingMiddleware)
  getOne(@Param("id") id: string) {
    // ...
  }
}

@UseBefore 在控制器操作之前执行中间件。@UseAfter 在每个控制器操作后执行中间件。

全局中间件

全局中间件在每个请求之前始终运行。要使您的中间件全局化,请使用 @Middleware 装饰器标记,并指定它是在控制器操作之前还是之后运行。

import { ExpressMiddlewareInterface } from 'typenexus';
import { Request, Response, NextFunction } from 'express';

@Middleware({ type: 'before' })
export class LoggingMiddleware implements ExpressMiddlewareInterface {
  use(request: Request, response: Response, next: NextFunction): void {
    console.log('do something...');
    // @ts-ignore
    request.test = 'wcj';
    next();
  }
}

要启用此中间件,请在 typenexus 初始化期间指定它:

import { TypeNexus } from 'typenexus';
import './LoggingMiddleware.js';

const app = new TypeNexus(3002, {
  routePrefix: '/api',
  developmentMode: false,
});

或在 app.controllers() 中注册。

import { TypeNexus } from 'typenexus';
import { LoggingMiddleware } from './LoggingMiddleware.js';
import { UserController } from './UserController.js';

const app = new TypeNexus(3002, {
  routePrefix: '/api',
  developmentMode: false,
});
app.controllers([UserController], [LoggingMiddleware]);

错误处理程序

错误处理程序仅针对 Express。错误处理程序与中间件的工作方式相同,但实现了 ExpressErrorMiddlewareInterface 接口:

创建一个实现 ErrorMiddlewareInterface 接口的类:

import { Middleware, ExpressErrorMiddlewareInterface } from 'typenexus';
import { Request, Response, NextFunction } from 'express';

@Middleware({ type: 'after' })
export class CustomErrorHandler implements ExpressErrorMiddlewareInterface {
  error(error: any, request: Request, response: Response, next: NextFunction): void {
    response.status(error.status || 500);
    next();
  }
}

自定义错误处理程序在默认错误处理程序之后被调用,因此您将无法更改响应代码或标头。为了防止这种情况,您必须通过在 TypeNexusOptions 中指定 defaultErrorHandler 选项来禁用默认错误处理程序:

import { TypeNexus } from 'typenexus';

const app = new TypeNexus(3002, {
  routePrefix: '/api',
  developmentMode: false,
  defaultErrorHandler: false, // 禁用默认错误处理程序,只有当您有自己的错误处理程序时才需要
});

贡献者

感谢我们的优秀贡献者们!

使用 contributors 制作。

许可证

本软件包采用 MIT 许可证授权。