/*
* Copyright 2018-2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict'
/**
* Plugin hooks setup module.
* Handles all Fastify hooks registration for CloudEvents.
* @module plugin
*/
const { CloudEventTransformer } = require('cloudevent')
/**
* Setup and register all CloudEvent hooks with Fastify.
*
* @param {!object} fastify Fastify instance
* @param {!object} options Hook configuration options
* @param {!object} builders Builders configuration options
*
* @inner
*/
function setupHooks (fastify, options, builders) {
const {
baseNamespace = null,
idGenerator = null,
cloudEventOptions = null,
cloudEventExtensions = null,
includeHttpAttributes = false,
includeRedundantAttributes = false,
onRequestAbortCallback = null,
onRequestCallback = null,
preParsingCallback = null,
preValidationCallback = null,
preHandlerCallback = null,
preSerializationCallback = null,
onErrorCallback = null,
onSendCallback = null,
onResponseCallback = null,
onTimeoutCallback = null,
onCloseCallback = null,
onRouteCallback = null,
onRegisterCallback = null,
onReadyCallback = null,
onListenCallback = null
} = options
// handle hooks, only when related callback are defined
// see [Hooks - Fastify reference - GitHub](https://github.com/fastify/fastify/blob/main/docs/Reference/Hooks.md)
if (onRequestAbortCallback !== null) {
fastify.addHook('onRequestAbort', async (request) => {
// small optimization: pass a null reply because no useful here
const ce = builders.buildCloudEventForHook('onRequestAbort', request, null)
// add some http related attributes to data, could be useful to have
if (includeHttpAttributes !== null && includeHttpAttributes === true) {
ce.data.request.httpVersion = request.raw.httpVersion
ce.data.request.originalUrl = request.raw.originalUrl
ce.data.request.upgrade = request.raw.upgrade
}
// console.log(`DEBUG - onRequestAbort: created CloudEvent ${CloudEventTransformer.dumpObject(ce, 'ce')}`)
// send the event to the callback
await onRequestAbortCallback(ce)
})
}
if (onRequestCallback !== null) {
fastify.addHook('onRequest', async (request, reply) => {
// small optimization: pass a null reply because no useful here
const ce = builders.buildCloudEventForHook('onRequest', request, null)
// add some http related attributes to data, could be useful to have
if (includeHttpAttributes !== null && includeHttpAttributes === true) {
ce.data.request.httpVersion = request.raw.httpVersion
ce.data.request.originalUrl = request.raw.originalUrl
ce.data.request.upgrade = request.raw.upgrade
}
// console.log(`DEBUG - onRequest: created CloudEvent ${CloudEventTransformer.dumpObject(ce, 'ce')}`)
// send the event to the callback
await onRequestCallback(ce)
})
}
if (preParsingCallback !== null) {
fastify.addHook('preParsing', async (request, reply, payload) => {
const ce = builders.buildCloudEventForHook('preParsing', request, reply, // payload)
null) // do not pass payload here or a "Converting circular structure to JSON" will be raised if enabled ...
await preParsingCallback(ce)
})
}
if (preValidationCallback !== null) {
fastify.addHook('preValidation', async (request, reply) => {
const ce = builders.buildCloudEventForHook('preValidation', request, reply)
await preValidationCallback(ce)
})
}
if (preHandlerCallback !== null) {
fastify.addHook('preHandler', async (request, reply) => {
const ce = builders.buildCloudEventForHook('preHandler', request, reply)
await preHandlerCallback(ce)
})
}
if (preSerializationCallback !== null) {
fastify.addHook('preSerialization', async (request, reply, payload) => {
const ce = builders.buildCloudEventForHook('preSerialization', request, reply, payload)
await preSerializationCallback(ce)
})
}
if (onErrorCallback !== null) {
fastify.addHook('onError', async (request, reply, error) => {
const processInfoAsData = CloudEventTransformer.processInfoToData()
const errorAsData = CloudEventTransformer.errorToData(error, {
includeStackTrace: true,
addStatus: true,
addTimestamp: true
})
const ceData = {
request: builders.buildRequestDataForCE(request),
reply: builders.buildReplyDataForCE(reply),
error: errorAsData,
process: processInfoAsData
}
if (includeRedundantAttributes !== null && includeRedundantAttributes === true) {
ceData.id = request.id
ceData.timestamp = CloudEventTransformer.timestampToNumber()
}
const ce = new fastify.CloudEvent(idGenerator.next().value,
`${baseNamespace}.onError`,
builders.buildSourceUrl(request.url),
ceData,
cloudEventOptions,
cloudEventExtensions
)
await onErrorCallback(ce)
// done() // do not pass the error to the done callback here
})
}
if (onSendCallback !== null) {
fastify.addHook('onSend', async (request, reply, payload) => {
const ce = builders.buildCloudEventForHook('onSend', request, reply, payload)
await onSendCallback(ce)
})
}
if (onResponseCallback !== null) {
fastify.addHook('onResponse', async (request, reply) => {
const ce = builders.buildCloudEventForHook('onResponse', request, reply)
// keep the request attribute from data, even if more data will be shown here
await onResponseCallback(ce)
})
}
if (onTimeoutCallback !== null) {
fastify.addHook('onTimeout', async (request, reply) => {
const ce = builders.buildCloudEventForHook('onTimeout', request, reply)
// keep the request attribute from data, even if more data will be shown here
await onTimeoutCallback(ce)
})
}
if (onCloseCallback !== null) {
// hook to plugin shutdown
fastify.addHook('onClose', async (instance) => {
const ce = new fastify.CloudEvent(idGenerator.next().value,
`${baseNamespace}.onClose`,
builders.buildSourceUrl(),
builders.buildPluginDataForCE('plugin shutdown'), // data
cloudEventOptions,
cloudEventExtensions
)
await onCloseCallback(ce)
})
}
if (onRouteCallback !== null) {
fastify.addHook('onRoute', (routeOptions) => {
const ce = new fastify.CloudEvent(idGenerator.next().value,
`${baseNamespace}.onRoute`,
builders.buildSourceUrl(),
routeOptions, // data
cloudEventOptions,
cloudEventExtensions
)
onRouteCallback(ce)
})
}
if (onRegisterCallback !== null) {
fastify.addHook('onRegister', (instance, opts) => {
const ce = new fastify.CloudEvent(idGenerator.next().value,
`${baseNamespace}.onRegister`,
builders.buildSourceUrl(),
builders.buildPluginDataForCE(`plugin registration, with options: ${JSON.stringify(opts)}`), // data
cloudEventOptions,
cloudEventExtensions
)
onRegisterCallback(ce)
})
}
if (onReadyCallback !== null) {
// triggered before the server starts listening for requests
fastify.addHook('onReady', async () => {
const ce = new fastify.CloudEvent(idGenerator.next().value,
`${baseNamespace}.onReady`,
builders.buildSourceUrl(),
builders.buildPluginDataForCE('plugin ready'), // data
cloudEventOptions,
cloudEventExtensions
)
await onReadyCallback(ce)
})
}
if (onListenCallback !== null) {
// triggered when the server starts listening for requests
fastify.addHook('onListen', async () => {
const ce = new fastify.CloudEvent(idGenerator.next().value,
`${baseNamespace}.onListen`,
builders.buildSourceUrl(),
builders.buildPluginDataForCE('server listening'), // data
cloudEventOptions,
cloudEventExtensions
)
await onListenCallback(ce)
})
}
// done()
}
module.exports = setupHooks