top
Using my app is also a way to support me:
Scap: Screenshot & Markup Edit Screen Test Deskmark Keyzer Vidwall Hub VidCrop Vidwall Mousio Hint Mousio Musicer Audioer FileSentinel FocusCursor Videoer KeyClicker DayBar Iconed Menuist Quick RSS Quick RSS Web Serve Copybook Generator DevTutor for SwiftUI RegexMate Time Passage Iconize Folder Textsound Saver Create Custom Symbols DevHub Resume Revise Palette Genius Symbol Scribe

TypeNexus

Buy me a coffee Follow On X Build & Test English NPM Downloads NPM version typeorm@^0.3.12 express@^4.18.2

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, CreateDateColumn } from 'typenexus';
// 或者:
import { Entity, PrimaryGeneratedColumn, Column, 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 参数,使用 @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 类来创建和使用自己的错误。要定义返回给客户端的数据,你可以在错误中定义一个 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 许可证授权。