This example of creating a general-purpose "users" plugin demonstrates that, with the help of schwifty, it's possible to create highly reusable, data-oriented hapi plugins.
Schwifty is a hapi plugin to integrate Objection ORM into hapi. One of its strengths is associating models and migrations with hapi plugins. When writing reusable plugins that declare models, it becomes desirable to be able to extend those models in application-level plugins.
Note
We're actively working on a slicker way of creating highly customizable plugins, so be on the lookout!
Also, this tutorial does not assume that your project is based upon the pal boilerplate, but if you are using it, the same approach should work. Just remember, calls to
server.register()
amount to placing files in theplugins/
dir, calls toserver.schwifty()
amount to placing model files in themodels/
dir, etc. thanks to the haute-couture plugin composer. You might also consider utilizing the pal boilerplate's Objection ORM flavor.
Okay, so here's the setup! We have a hapi plugin my-user-plugin
that provides a User
model and user-related CRUD/auth routes. We also have a plugin that implements the "meat" of our application, my-app-plugin
, and we want this application to have users.
The natural step to take would be for my-app-plugin
to simply register my-user-plugin
in order to add all the user-related functionality to our application. This actually works great, especially under schwifty's plugin ownership of knex instances and models! Complications arise as soon as we want to make my-user-plugin
more general-purpose, adding user functionality to any application.
For example, one application might be for a marketplace where each user has a shippingAddress
, and the other might be for a clown school where each user has a noseColor
. We can't expect my-user-plugin
to account on its own for every such case. But we can write my-user-plugin
in such a way that my-app-plugin
can provide details about the user model in a general-purpose way, whether it concerns shipping addresses or clown noses.
Okay, time for some code. Here's our app before the user plugin is properly generalized. By the end of this article, my-app-plugin
should be able to leverage my-user-plugin
to provide user functionality, but also specify an application-specific field, noseColor
, on users of our clown school app.
const Hapi = require('@hapi/hapi');
const Schwifty = require('@hapipal/schwifty');
const AppPlugin = require('./app-plugin');
(async () => {
const server = Hapi.server();
await server.register([
AppPlugin,
{
register: Schwifty,
options: {
knex: {
client: 'sqlite3',
useNullAsDefault: true,
connection: {
filename: ':memory:'
}
}
}
}
]);
await server.start();
console.log(`Ready to go! See ${server.info.uri}`);
})();
const UserPlugin = require('../user-plugin');
exports.plugin = {
name: 'my-app-plugin',
async register(server, options) {
await server.register({
register: UserPlugin,
options: {}
});
}
};
const Schwifty = require('@hapipal/schwifty');
const UserModel = require('./user-model');
exports.plugin = {
name: 'my-user-plugin',
async register(server, options) {
await server.register(Schwifty);
// Register the user model
server.schwifty(UserModel);
// Get all users
server.route({
method: 'get',
path: '/users',
handler: async (request) => {
const { Users } = request.models();
return await Users.query();
}
});
}
};
const Joi = require('joi');
const Schwifty = require('@hapipal/schwifty');
// A user model only with an id and name
module.exports = class User extends Schwifty.Model {
static get tableName() {
return 'Users';
}
static get joiSchema() {
return Joi.object({
id: Joi.number().required(),
name: Joi.string().required()
});
}
};
The approach we're going to take is,
- Expose
my-user-plugin
'sUser
model by exporting it. - Extend
my-user-plugin
'sUser
model insidemy-app-plugin
, adding a new field. - Pass the extended
User
model tomy-user-plugin
as a plugin option,options.User
. - Within
my-user-plugin
utilizeoptions.User
.- If it's not passed, use own base user model.
- If it is passed, ensure that
options.User
extends from the base user model and has the same classname
, then useserver.schwifty(options.User)
.
This approach allows my-user-plugin
to remain the "owner" of the user model while allowing my-app-plugin
to have a simple hook to adjust it however necessary. It also allows my-user-plugin
to ensure that my-app-plugin
has only made acceptable adjustments to the model if needed– in this case it ensures that the application plugin has extended the correct base user model. Let's step through these code changes.
Exposing the user plugin's model will allow the app plugin to extend it.
const Schwifty = require('@hapipal/schwifty');
const UserModel = require('./user-model');
+exports.Model = UserModel;
+
exports.plugin = {
name: 'my-user-plugin',
async register(server, options) {
Now the app plugin has access to the user plugin's model and may extend it however it sees fit. We'll add a noseColor
attribute to the model. Notice that Joi's object.keys()
is used to amend the base user's Joi schema, maintaining its id
and name
attributes defined in the user plugin. For consistency make sure to name the model class User
, identical to the name assigned to the model by the user plugin. Note that you may also create a migration to add this additional column within my-app-plugin
's migrations directory.
const Joi = require('joi');
const UserPlugin = require('../user-plugin');
module.exports = class User extends UserPlugin.Model {
static get joiSchema() {
return super.joiSchema.keys({
noseColor: Joi.string().valid('red', 'blue', 'pink')
});
}
};
Now we'll pass the app's extended user model as a plugin option to the user plugin. This will allow the user plugin to register it with schwifty if it passes muster.
const UserPlugin = require('../user-plugin');
exports.plugin = {
name: 'my-app-plugin',
async register(server, options) {
await server.register({
register: UserPlugin,
- options: {}
+ options: {
+ User: require('./user-model')
+ }
});
}
};
Now all the user plugin has to do is ensure that options.User
is compatible with its base user model, and if so then register it with schwifty instead of the base model. Schwifty.assertCompatible(ModelA, ModelB)
is a utility provided by schwifty to ensure basic compatibility of two models: one model extends the other, they have the same name
, and the same tableName
.
exports.plugin = {
name: 'my-user-plugin',
async register(server, options) {
await server.register(Schwifty);
// Register the user model
- server.schwifty(UserModel);
+
+ if (options.User) {
+ Schwifty.assertCompatible(options.User, UserModel);
+ }
+
+ server.schwifty(options.User || UserModel);
// Get all users
server.route({
method: 'get',
path: '/users',
This simple technique allows you to create plugins that define schwifty models in an extensible way. It's incredibly useful to create general-purpose plugins that can be used within many applications, and when it comes to using hapi plugins with Objection, this is exactly how to do it.