{"version":3,"file":"webhook-runner.mjs","sources":["../../src/services/webhook-runner.ts"],"sourcesContent":["/**\n * The event hub is Strapi's event control center.\n */\n\nimport createdDebugger from 'debug';\nimport _ from 'lodash';\nimport type { Logger } from '@strapi/logger';\n\nimport type { Modules } from '@strapi/types';\nimport WorkerQueue from './worker-queue';\nimport type { EventHub } from './event-hub';\nimport type { Fetch } from '../utils/fetch';\n\ntype Webhook = Modules.WebhookStore.Webhook;\n\ninterface Config {\n  defaultHeaders: Record<string, string>;\n}\n\ninterface ConstructorParameters {\n  eventHub: EventHub;\n  logger: Logger;\n  configuration?: Record<string, unknown>;\n  fetch: Fetch;\n}\n\ninterface Event {\n  event: string;\n  info: Record<string, unknown>;\n}\n\ntype Listener = (info: Record<string, unknown>) => Promise<void>;\n\nconst debug = createdDebugger('strapi:webhook');\n\nconst defaultConfiguration: Config = {\n  defaultHeaders: {},\n};\n\nclass WebhookRunner {\n  private eventHub: EventHub;\n\n  private logger: Logger;\n\n  private config: Config;\n\n  private webhooksMap: Map<string, Webhook[]> = new Map();\n\n  private listeners: Map<string, Listener> = new Map();\n\n  private queue: WorkerQueue<Event, void>;\n\n  private fetch: Fetch;\n\n  constructor({ eventHub, logger, configuration = {}, fetch }: ConstructorParameters) {\n    debug('Initialized webhook runner');\n    this.eventHub = eventHub;\n    this.logger = logger;\n    this.fetch = fetch;\n\n    if (typeof configuration !== 'object') {\n      throw new Error(\n        'Invalid configuration provided to the webhookRunner.\\nCheck your server.json -> webhooks configuration'\n      );\n    }\n\n    this.config = _.merge(defaultConfiguration, configuration);\n\n    this.queue = new WorkerQueue({ logger, concurrency: 5 });\n\n    this.queue.subscribe(this.executeListener.bind(this));\n  }\n\n  deleteListener(event: string) {\n    debug(`Deleting listener for event '${event}'`);\n\n    const fn = this.listeners.get(event);\n\n    if (fn !== undefined) {\n      this.eventHub.off(event, fn);\n      this.listeners.delete(event);\n    }\n  }\n\n  createListener(event: string) {\n    debug(`Creating listener for event '${event}'`);\n    if (this.listeners.has(event)) {\n      this.logger.error(\n        `The webhook runner is already listening for the event '${event}'. Did you mean to call .register() ?`\n      );\n    }\n\n    const listen = async (info: Event['info']) => {\n      this.queue.enqueue({ event, info });\n    };\n\n    this.listeners.set(event, listen);\n    this.eventHub.on(event, listen);\n  }\n\n  async executeListener({ event, info }: Event) {\n    debug(`Executing webhook for event '${event}'`);\n    const webhooks = this.webhooksMap.get(event) || [];\n    const activeWebhooks = webhooks.filter((webhook) => webhook.isEnabled === true);\n\n    for (const webhook of activeWebhooks) {\n      await this.run(webhook, event, info).catch((error: unknown) => {\n        this.logger.error('Error running webhook');\n        this.logger.error(error);\n      });\n    }\n  }\n\n  run(webhook: Webhook, event: string, info = {}) {\n    const { url, headers } = webhook;\n\n    return this.fetch(url, {\n      method: 'post',\n      body: JSON.stringify({\n        event,\n        createdAt: new Date(),\n        ...info,\n      }),\n      headers: {\n        ...this.config.defaultHeaders,\n        ...headers,\n        'X-Strapi-Event': event,\n        'Content-Type': 'application/json',\n      },\n      signal: AbortSignal.timeout(10000),\n    })\n      .then(async (res) => {\n        if (res.ok) {\n          return {\n            statusCode: res.status,\n          };\n        }\n\n        return {\n          statusCode: res.status,\n          message: await res.text(),\n        };\n      })\n      .catch((err) => {\n        return {\n          statusCode: 500,\n          message: err.message,\n        };\n      });\n  }\n\n  add(webhook: Webhook) {\n    debug(`Registering webhook '${webhook.id}'`);\n    const { events } = webhook;\n\n    events.forEach((event) => {\n      if (this.webhooksMap.has(event)) {\n        this.webhooksMap.get(event)?.push(webhook);\n      } else {\n        this.webhooksMap.set(event, [webhook]);\n        this.createListener(event);\n      }\n    });\n  }\n\n  update(webhook: Webhook) {\n    debug(`Refreshing webhook '${webhook.id}'`);\n    this.remove(webhook);\n    this.add(webhook);\n  }\n\n  remove(webhook: Webhook) {\n    debug(`Unregistering webhook '${webhook.id}'`);\n\n    this.webhooksMap.forEach((webhooks, event) => {\n      const filteredWebhooks = webhooks.filter((value) => value.id !== webhook.id);\n\n      // Cleanup hanging listeners\n      if (filteredWebhooks.length === 0) {\n        this.webhooksMap.delete(event);\n        this.deleteListener(event);\n      } else {\n        this.webhooksMap.set(event, filteredWebhooks);\n      }\n    });\n  }\n}\n\n/**\n * Expose a factory function instead of the class\n */\nexport default function createWebhookRunner(opts: ConstructorParameters): WebhookRunner {\n  return new WebhookRunner(opts);\n}\n\nexport type { WebhookRunner };\n"],"names":["createdDebugger"],"mappings":"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"}