Hooks ​
Hooks are simply funtions that are run before and after a context hits the related service.
Let's say we only allow admins to create users in our app and also send email to the user after the created. We could have a chain of hooks in the following format
authenticateUser -> checkUserIsAdmin -> [ user-service:create ] -> sendEmail
authenticateUser -> checkUserIsAdmin -> [ user-service:create ] -> sendEmail
authenticateUser
can be implemented to decode theAuthorization
entry from the headers. After decoding, the hook can assign it tocontext.user
.checkUserIsAdmin
checks thatuser.role === 'admin'
, else it throws an Unathorized error- The service goes ahead and creates the user
- The context is then passed to
sendEmail
; here a SendGrid API can be called to send a welcome email.
The signature for any hook is in the format:
async function hookName(context: Context, app: App) {
// Do something, with `context` maybe
return context
}
async function hookName(context: Context, app: App) {
// Do something, with `context` maybe
return context
}
What this means is that, hooks are reusable by default. You can reuse a hook for many services.
How to use hooks ​
Generally, you'll use the UI to compose your hooks. Mangobase ships with a number of relevant hooks you can use with your collections without having to write any by hand (in code).
Here's a video on how to use the UI/Dashboard to compose hooks.
The hooks demonstrated in the video are known as plugin
hooks. They are installed on app.hooksRegistry
and become available for use in the hooks editor of each collection.
Registering a plugin hook ​
To register a plugin hook so that it becomes available in the hooks editor, you can follow this example:
// First define the hook
const AllowAdminsOnly: Hook {
id: 'allow-admins',
name: 'Allow Admins',
run: async (ctx, config, app) {
if (!ctx.user) {
throw new app.errors.BadRequest('user is required')
}
if (ctx.user.role !== 'admin') {
throw new app.errors.Unauthorized()
}
return ctx
}
}
// then register it on the hook registry
app.hooksRegistry.register(AllowAdminsOnly)
// First define the hook
const AllowAdminsOnly: Hook {
id: 'allow-admins',
name: 'Allow Admins',
run: async (ctx, config, app) {
if (!ctx.user) {
throw new app.errors.BadRequest('user is required')
}
if (ctx.user.role !== 'admin') {
throw new app.errors.Unauthorized()
}
return ctx
}
}
// then register it on the hook registry
app.hooksRegistry.register(AllowAdminsOnly)
INFO
You can see more varied examples from here.
App level hooks ​
Here's an implementation for authenticateUser
as an example. This hook will be registered on the app. This means, the hook will be called for every request (regardless of the service).
async function authenticateUser(context: Context, app: App) {
const { authorization } = context.headers
const jwtUser = decodeAuthorizationJwt(authorization)
const usersCollection = await app.manifest.collection('users)
const user = await usersCollection.find(jwtUser.id)
context.user = user
}
async function authenticateUser(context: Context, app: App) {
const { authorization } = context.headers
const jwtUser = decodeAuthorizationJwt(authorization)
const usersCollection = await app.manifest.collection('users)
const user = await usersCollection.find(jwtUser.id)
context.user = user
}
Here's how you register it with the app:
import { authenticateUser } from './authenticate'
const app = new App({})
app.before(authenticate)
import { authenticateUser } from './authenticate'
const app = new App({})
app.before(authenticate)
There's also a corresponding after
method, to register after hooks. You can register as many hooks as you want.
Service level hooks ​
Let's implement sendEmail
as a demonstration:
async function sendEmail(context: Context, app: App) {
// since we expect this hook to be in the `after` stage
// we can throw an error to expect `result` on the context
if (!context.result) {
throw new app.errors.ServiceError(
'missing `result` on the context. make sure this hook is registered as an after-hook.'
)
}
const user = context.result as User
await sendWelcomeEmail({ email: user.email })
return context
}
async function sendEmail(context: Context, app: App) {
// since we expect this hook to be in the `after` stage
// we can throw an error to expect `result` on the context
if (!context.result) {
throw new app.errors.ServiceError(
'missing `result` on the context. make sure this hook is registered as an after-hook.'
)
}
const user = context.result as User
await sendWelcomeEmail({ email: user.email })
return context
}
import { sendEmail } from './send-email'
const app = new App({})
// assuming we already registered a user service pipeline
const usersPipeline = app.pipeline('users')
usersPipeline.after(sendEmail)
import { sendEmail } from './send-email'
const app = new App({})
// assuming we already registered a user service pipeline
const usersPipeline = app.pipeline('users')
usersPipeline.after(sendEmail)
TIP
See API docs for what Pipeline is.
Error hooks ​
When an error is thrown in a pipeline, all subsequent before or after hooks for the service and the app are not called. The current context is then passed to all error hooks registered on the app. Error hooks can only be installed on the app
const app = new App({ })
app.error(async (ctx, app) => {
// report error to sentry, maybe
})
const app = new App({ })
app.error(async (ctx, app) => {
// report error to sentry, maybe
})