How to extend Tensei applications using plugins
A plugin may be used to extend Tensei. There's a ton of things you can do with plugins. We have many official plugins such as auth
, rest
, graphql
, and media
. This shows you how powerful plugins can be.
To get a good understanding of plugins, you'll need to understand the life cycle of a Tensei application.
When a Tensei app is started, it goes through the following steps:
register
. At this point, plugins can add resources, fields, middleware, hooks, assets and a lot more to the Tensei application.boot
. At this point, plugins have access to the database and also have access to all other resources, hooks, fields registered by other plugins.You may create a plugin by calling the plugin
function from Tensei. Let's create an example plugin called blog.
import { tensei } from '@tensei/core'
import { plugin } from '@tensei/common'
// Create a plugin
const blog = plugin('Blog Plugin')
// Register a plugin on a Tensei application
export default tensei()
.plugins([
blog
])
This plugin will help any member of the Tensei community setup a blog in a few minutes. To do this, here's what our plugin will do:
Let's get started.
First let's define our blog resources:
import { resource, text, textarea, hasMany, belongsToMany } from '@tensei/common'
const postResource = resource('Post').fields([
text('Title'),
textarea('Content'),
belongsTo('Category'),
belongsToMany('Tag')
])
const categoryResource = resource('Category').fields([
text('Name'),
textarea('Description'),
hasMany('Post')
])
const tagResource = resource('Tag').fields([
text('Name'),
textarea('Description'),
belongsToMany('Post')
])
In a plugin, we can register custom resources, hooks, routes, graphql queries and so much more. We do this by calling the .register()
method on a plugin and passing a callback. When Tensei is starting the application, it'll register our plugin by calling this callback.
import { plugin } from '@tensei/common'
const blog = plugin('Blog Plugin')
.register(({
extendResources,
extendRoutes,
extendGraphQlTypeDefs,
extendGraphQlQueries,
extendCommands,
extendEvents
}) => {
extendResources([postResource, tagResource, categoryResource])
})
The register hook receives a list of helpful methods we can use to extend the Tensei core. Some of these are:
extendResources
to register new resourcesextendCommands
to register new CLI commands to the application from a pluginextendGraphQlTypeDefs
to add custom type definitions to the graphql schema.extendGraphQlQueries
to add custom graphql queries (mutations) to the graphql schema.extendRoutes
to add custom REST API routes or just normal routes to the Express application.extendEvents
to add custom event listeners to the coreIn our case, during registration, we are adding three new resources to the core.
From a plugin, you may register custom routes on the application using the extendRoutes
register function. Let's add a route for fetching blog statistics. Our focus won't be the statistics itself, but rather the custom route registration:
import { plugin, route } from '@tensei/common'
// Define a route
const statsRoute = route('Fetch blog stats')
.get()
.path('/blog/stats')
.handle(async (({ db }), response) => (response.json({
totalPosts: await db.posts().count(),
totalCategories: await db.categories().count(),
})))
const blog = plugin('Blog Plugin')
.register(({
extendResources,
extendRoutes,
}) => {
extendResources([postResource, tagResource, categoryResource])
// Extend routes in the core
extendRoutes([statsRoute])
})
From a plugin, you may extend the graphql schema by adding custom type definitions and queries or mutations. Let's add a query to fetch statistics about the blog. First, we'll add a type definition, then we'll add a query:
import { plugin, graphQlQuery } from '@tensei/common'
const typeDefs = /* GraphQL */ `
type BlogStatistics {
totalPosts: Int!
totalCategories: Int!
}
extend type Query {
blogStats: BlogStatistics
}
`
const statsQuery = graphQlQuery('Get blog statistics')
.query()
.path('blogStats')
.handle((source, args, { db }) => ({
totalPosts: () => db.posts().count(),
totalCategories: () => db.categories().count(),
}))
export const blog = plugin('Blog Plugin')
.register(({
extendResources,
extendRoutes,
extendGraphQlTypeDefs,
extendGraphQlQueries
}) => {
// Extend resources in the core
extendResources([postResource, tagResource, categoryResource])
// Extend routes in the core
extendRoutes([statsRoute])
// Extend graphql types
extendGraphQlTypeDefs([typeDefs])
// Extend graphql queries
extendGraphQlQueries([statsQuery])
})
We used the extendGraphQlTypeDefs
and extendGraphQlQueries
methods to add types and queries to the graphql schema.
Let's say we wanted to subscribe to events from another plugin. As an example, let's say we wanted to send an email if a user registered a new account. User registration is handled by the auth plugin, and after a successful registration, the user::registered
event is emitted. With custom event listeners, we can subscribe to this event and perform an action when this event is triggered.
First we'll create a new event listener, and then we'll register it:
import { event } from '@tensei/common'
const sendStatsEmailAfterRegistration = event('user::registered')
.listen(async ({ payload, ctx }) => {
const stats = {
totalPosts: await ctx.db.posts().count(),
totalCategories: await ctx.db.categories().count(),
}
ctx.mailer.send(
(message) => {
message
.to(payload.email)
.html(`The latest stats are: Total posts: ${stats.totalPosts}, Total categories: ${stats.totalCategories}`)
}
)
})
const blog = plugin('Blog Plugin')
.register(({
extendEvents,
}) => {
extendEvents([sendStatsEmailAfterRegistration])
})
After the database connection is established and all plugins have been registered, we want to add one new category to the blog. We may do this in the boot
hook. This hook is executed by the Tensei core during its life cycle.
The .boot()
. We also have access to the ORM and can use it to perform database queries. Let's add it here:
import { plugin } from '@tensei/common'
const blog = plugin('Blog Plugin')
.register(({
extendResources,
extendRoutes,
}) => {
extendResources([postResource, tagResource, categoryResource])
extendRoutes([statsRoute])
extendGraphQlTypeDefs([typeDefs])
extendGraphQlQueries([statsQuery])
})
.boot(async ({ db }) => {
// Attempt to find a category with the name of General from the database.
const generalCategory = await db.categories().findOne({ name: 'General' })
// Create the General category if it does not yet exist in the database.
if (! generalCategory) {
// Create a new category instance
const newCategory = db.categories().create({
name: 'General'
})
// Persist the new category to the database.
await db.categories().persistAndFlush(newCategory)
}
})
In the above snippet, we attempt to find a General
category in the database, and if it doesn't exist, we create it. Remember that this is only executed during the Tensei boot process. This would not be executed when your application is receiving requests.