自定义API钩子 Custom API Hooks

有关如何在 Directus 中构建自定义挂钩的指南。自定义 API 挂钩允许在项目中发生指定事件时运行自定义逻辑。 有不同类型的事件可供选择。

扩展入口点

钩子的入口点是扩展包的 src/ 文件夹中的 index 文件。 它导出一个注册函数来注册一个或多个事件监听器。

入口点示例:

js
export default ({ filter, action }) => {
  filter('items.create', () => {
    console.log('Creating Item!')
  })

  action('items.create', () => {
    console.log('Item created!')
  })
}

Events

您的挂钩可以触发各种不同的事件。 事件由其类型和名称定义。 有五种事件类型可供选择:

如果您希望钩子在事件发生前触发,请使用过滤器钩子。 当您希望挂钩在事件发生后触发时,请使用动作挂钩。

Filter

过滤器挂钩在事件被触发之前作用于事件的有效负载。 它们允许您检查、修改或取消事件。

下面是通过抛出标准 Directus 异常来取消“创建”事件的示例。

js
export default ({ filter }, { exceptions }) => {
  const { InvalidPayloadException } = exceptions

  filter('items.create', async (input) => {
    if (LOGIC_TO_CANCEL_EVENT)
      throw new InvalidPayloadException(WHAT_IS_WRONG)

    return input
  })
}

过滤器寄存器函数接收两个参数:

  • 事件名称
  • 每当事件触发时执行的回调函数。

回调函数本身接收三个参数:

  • 可修改的有效载荷
  • 特定于事件的元对象
  • 上下文对象

上下文对象具有以下属性:

  • database — 当前数据库事务
  • schema — 当前使用的 API 模式
  • accountability — 当前用户的信息

表现
过滤器在不小心实施时会影响性能,因为它们以阻塞方式执行。 这尤其适用于触发“读取”事件的过滤器,其中单个请求可能导致大量数据库读取。

Action

动作挂钩在定义的事件之后执行并接收与事件相关的数据。 当您需要自动响应项目或服务器操作的 CRUD 事件时,使用操作挂钩。

动作寄存器函数接收两个参数:

  • 事件名称
  • 每当事件触发时执行的回调函数。

回调函数本身接收两个参数:

  • 特定于事件的元对象
  • 上下文对象

上下文对象具有以下属性:

  • database — 当前数据库事务
  • schema — 当前使用的 API 模式
  • accountability — 关于当前用户的信息

Init

Init 挂钩在 Directus 生命周期内的定义点执行。 使用 init 挂钩对象将逻辑注入内部服务。

初始化寄存器函数接收两个参数:

  • 事件名称
  • 每当事件触发时执行的回调函数。

回调函数本身接收一个参数:

  • 特定于事件的元对象

Schedule

计划挂钩在特定时间点执行,而不是在 Directus 执行特定操作时执行。 这是通过 node-cron 支持的。

要设置计划事件,请提供一个 cron 语句作为“schedule()”函数的第一个参数。 例如 schedule('15 14 1 * *', <...>)(在第 1 天的 14:15)或 schedule('5 4 * * sun', <...> )(周日 04:05)。

下面是一个注册调度挂钩的例子。

js
import axios from 'axios'

export default ({ schedule }) => {
  schedule('*/15 * * * *', async () => {
    await axios.post('http://example.com/webhook', { message: 'Another 15 minutes passed...' })
  })
}

Embed

将自定义 JavaScript 或 CSS 注入数据洞察中的“”和“”标签。

嵌入寄存器函数接收两个参数:

  • 嵌入的位置,headbody
  • 要嵌入的值,可以是字符串或返回字符串的函数。

下面是注册嵌入挂钩的示例。

js
export default ({ embed }, { env }) => {
  // Google Tag Manager Example
  embed(
    'head',
    () => `<!-- Google Tag Manager -->
  <script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','${env.GTM_ID}');</script>
  <!-- End Google Tag Manager -->`
  )

  // Sentry Example
  embed(
    'head',
    '<script src="https://browser.sentry-cdn.com/7.21.1/bundle.min.js" integrity="sha384-xOL2QebDu7YNMtC6jW2i5RpQ5RcWOyQMTwrWBiEDezpjjXM7mXhYGz3vze77V91Q" crossorigin="anonymous"></script>'
  )
  embed(
    'body',
    () => `<script>
  Sentry.init({
   dsn: "${env.SENTRY_DSN}" // "https://examplePublicKey@o0.ingest.sentry.io/0",
   release: "my-project-name@${env.npm_package_version}",
   integrations: [new Sentry.BrowserTracing()],

   // We recommend adjusting this value in production, or using tracesSampler
   // for finer control
   tracesSampleRate: 1.0,
  });
  </script>`
  )
}

Available Events

Filter Events

NamePayloadMeta
request.not_foundfalserequest, response
request.error请求错误--
database.error数据库错误client
auth.login登录负载status, user, provider
auth.jwt授权令牌status, user, provider, type
authenticate空的责任对象req
(<collection>.)items.query项目查询collection
(<collection>.)items.read阅读的项目query, collection
(<collection>.)items.create新项目collection
(<collection>.)items.update更新的项目keys, collection
(<collection>.)items.delete物品的钥匙collection
<system-collection>.create新项目collection
<system-collection>.update更新的项目keys, collection
<system-collection>.delete物品的钥匙collection

System Collections
<system-collection> 应替换为系统集合名称之一 activity, collections, fields, files (except create/update), folders, permissions, presets, relations, revisions, roles, settings, users or webhooks.

Action Events

NameMeta
server.startserver
server.stopserver
responserequest, response, ip, duration, finished
auth.loginpayload, status, user, provider
files.uploadpayload, key, collection
(<collection>.)items.readpayload, query, collection
(<collection>.)items.createpayload, key, collection
(<collection>.)items.updatepayload, keys, collection
(<collection>.)items.deletekeys, collection
(<collection>.)items.sortcollection, item, to
<system-collection>.createpayload, key, collection
<system-collection>.updatepayload, keys, collection
<system-collection>.deletekeys, collection

System Collections
<system-collection> 应替换为系统集合名称之一 activity, collections, fields, files (except create/update), folders, permissions, presets, relations, revisions, roles, settings, users or webhooks.

Init Events

NameMeta
cli.beforeprogram
cli.afterprogram
app.beforeapp
app.afterapp
routes.beforeapp
routes.afterapp
routes.custom.beforeapp
routes.custom.afterapp
middlewares.beforeapp
middlewares.afterapp

Register Function

register 函数接收一个包含特定类型的 register 函数的对象作为第一个参数:

  • filter — 监听过滤器事件
  • action — 监听动作事件
  • init — 监听初始化事件
  • schedule — 在特定时间点执行函数

第二个参数是具有以下属性的上下文对象:

  • services — 所有 API 内部服务
  • exceptions — 可用于抛出“适当”错误的 API 异常对象
  • database — 连接到当前数据库的 Knex 实例
  • getSchema — 读取服务中使用的完整可用模式的异步函数
  • env — 解析的环境变量
  • loggerPino 实例。
  • emitter事件发射器 实例,可用于触发其他扩展的自定义事件。

事件循环
使用发射器实现自定义事件时,请确保您永远不会直接或间接发射您的钩子当前正在处理的相同事件,因为这会导致无限循环!

Example: Sync with External

js
import axios from 'axios'

export default ({ filter }, { services, exceptions }) => {
  const { MailService } = services
  const { ServiceUnavailableException, ForbiddenException } = exceptions

  // Sync with external recipes service, cancel creation on failure
  filter('items.create', async (input, { collection }, { schema, database }) => {
    if (collection !== 'recipes')
      return input

    const mailService = new MailService({ schema, knex: database })

    try {
      await axios.post('https://example.com/recipes', input)
      await mailService.send({
        to: 'person@example.com',
        template: {
          name: 'item-created',
          data: {
            collection,
          },
        },
      })
    }
    catch (error) {
      throw new ServiceUnavailableException(error)
    }

    input.syncedWithExample = true

    return input
  })
}