Schwifty is intended for use with hapi v19+, nodejs v12+, Joi v17+, Objection v1 through v3, and knex v0.16+ (see v5 for lower support).
Schwifty is used to define Joi-compatible models and knex connections for use with Objection ORM. Those models then become available within your hapi server where it is most convenient. It has been tailored to multi-plugin deployments, where each plugin may set clear boundaries in defining its own models, knex database connections, and migrations. It's safe to register schwifty multiple times, wherever you'd like to use it, as it protects against model name collisions and other ambiguous configurations.
// First, ensure your project includes knex, objection, and sqlite3// To get started you might run,// npm install --save @hapipal/schwifty @hapi/hapi joi knex objection sqlite3'use strict';constHapi=require('@hapi/hapi');constSchwifty=require('@hapipal/schwifty');constJoi=require('joi');(async()=>{constserver=Hapi.server({port: 3000});server.route({method: 'get',path: '/dogs/{id}',handler: async(request)=>{const{ Dog }=request.models();returnawaitDog.query().findById(request.params.id);}});awaitserver.register({plugin: Schwifty,options: {knex: {client: 'sqlite3',useNullAsDefault: true,connection: {filename: ':memory:'}}}});// Register a model with schwifty...server.registerModel(classDogextendsSchwifty.Model{statictableName='Dog';staticjoiSchema=Joi.object({id: Joi.number(),name: Joi.string()});});awaitserver.initialize();// ... then make a table ...constknex=server.knex();awaitknex.schema.createTable('Dog',(table)=>{table.increments('id').primary();table.string('name');});// ... then add some records ...const{ Dog }=server.models();awaitPromise.all([Dog.query().insert({name: 'Guinness'}),Dog.query().insert({name: 'Sully'}),Dog.query().insert({name: 'Ren'})]);// ... then start the server!awaitserver.start();console.log(`Now, go find some dogs at ${server.info.uri}`);})();
Schwifty is intended for use with hapi v19+, nodejs v12+, Joi v17+, Objection v1 through v3, and knex v0.16+ (see v5 for lower support).
The hapi plugin
Registration
Schwifty may be registered multiple times—it should be registered in any plugin that would like to use any of its features. Upon each registration these options are collected until server initialization. Knex configurations passed during registration are attributed to the registering plugin/server under schwifty's ownership system. Server initialization will fail if any knex instances handled by schwifty do not have basic database connectivity.
Schwifty takes the following registration options,
knex - a knex instance or configuration. It may only be specified once per plugin/server.
migrationsDir - specifies a directory of knex migrations. The directory path may be either absolute, relative to the server's path prefix when set, or otherwise relative to the current working directory. It may only be specified once per plugin/server.
migrateOnStart - a boolean, 'latest', or 'rollback', to determine how to handle knex migrations at server initialization time. Defaults to false, which indicates to not handle migrations at all. When true or 'latest', runs all migrations that have not been run. When 'rollback', rolls-back the latest group of migrations. It may only be specified once.
teardownOnStop - a boolean indicating whether or not all knex connections should be torn-down when the hapi server stops (after server connections are drained). Defaults to true, and may only be specified once.
Server decorations
server.registerModel(models)
Used to declare models on a per-plugin basis or on the root server. In other words, these models are particular to the current plugin under schwifty's ownership system.
The models should be an objection or schwifty model class, or an array of such model classes to be associated with the current plugin or root server. Note that this method protects from model name conflicts across the server.
server.knex([namespace])
Returns server's knex instance.
Per schwifty's plugin ownership of knex instances, this is the knex instance provided by server or the nearest knex instance among server's ancestors (e.g. if server is within a plugin that has been registered by a separate plugin that provides a knex instance) that is also not sandboxed.
By passing a namespace you can obtain knex instance from the perspective of a different plugin. When namespace is a string, you receive the knex instance that is visibile within the plugin named namespace. And when namespace is true, you receive the knex instance that is visible to the root server.
server.models([namespace])
Returns an object containing models keyed by their name.
Per schwifty's plugin ownership of models, the models that are available on this object are only those registered by server or any plugins for which server is an ancestor (e.g. if server has registered a plugin that registers models) that are also not sandboxed.
By passing a namespace you can obtain the models from the perspective of a different plugin. When namespace is a string, you receive models that are visibile within the plugin named namespace. And when namespace is true, you receive models that are visible to the root server: every model registered with the hapi server– across all plugins– that isn't sandboxed.
Request decorations
request.knex([namespace])
Returns a knex instance. See server.knex(), where server is the one in which the request's route was declared (i.e. based upon request.route.realm).
request.models([all])
Returns an object containing models keyed by their name. See server.models(), where server is the one in which the request's route was declared (i.e. based upon request.route.realm).
Response toolkit decorations
h.knex([namespace])
Returns a knex instance. See server.knex(), where server is the one in which the corresponding route or server extension was declared (i.e. based upon h.realm).
h.models([namespace])
Returns an object containing models keyed by their name. See server.models(), where server is the one in which the corresponding route or server extension was declared (i.e. based upon h.realm).
Plugin ownership of knex instances and models
How do plugins "own" knex instances and models?
Schwifty cares a whole lot about plugin boundaries. Plugins represent the structure of your application, and we think that's not only practical but also very meaningful. Under schwifty plugins declare knex instances and models, and actually retain ownership of them in a useful way that respects your application's plugin boundaries.
Knex instances
Whenever a plugin or the root server configures a knex instance by registering schwifty and passing knex, an instance of knex is attributed to that plugin. Consider plugin-x that declares a knex instance. It becomes available by calling server.knex() within plugin-x, or request.knex() within one of plugin-x's routes. But it goes further! This instance of knex is also available to all plugins registered byplugin-x that do not have their own knex instances.
This allows us to handle very common use-cases, e.g. a single knex/database is configured on the root server: all other plugins using schwifty are registered under the root server and automatically inherit that database configuration. But it also allows us to handle complex cases, e.g. multiple applications with their own knex/databases all being deployed together as separate plugins on a single hapi server.
Models
Whenever a plugin or the root server declares some models by calling server.registerModel(), those models are attributed to that plugin. Consider plugin-x registering plugin plugin-a, which declares the model Pets. Inside plugin-a the Pets model is available by calling server.models() within plugin-a, or request.models() within one of plugin-a's routes. But, just as with knex instances, it goes further! Pets is also available to plugin-x since it registered plugin-a. In fact, Pets is available to the entire plugin chain up to the root server. In this way, the root server will have access to every model declared by any plugin (barring sandboxing).
This allows us to handle very common use-cases, e.g. a plugin simply wants to declare and use some models. But it also allows us to handle complex cases, e.g. authoring a plugin that declares some models that you would like to reuse across multiple applications.
Note that the schmervice plugin deals with plugin ownership of services in exactly the same way.
As an escape hatch, you can always call server.models(true) (passing true to any of the server, request, or response toolkit's models() decoration) to break the plugin boundary and access models declared by any plugin on the entire server. You may similarly pass a plugin name in order to access models from the perspective of that specific plugin, e.g. server.models('my-plugin').
Sandboxing
When a model or knex instance is "sandboxed", it is only visible within the plugin that directly provides it. This is a form of opting-out of transitive ownership of models and knex instances described in the previous two sections. However, sandboxed models and knex instances can still be accessed from other plugins using the namespace parameter as described in server.models([namespace]) and server.knex([namespace])
In order to sandbox a model, set the Schwifty.sandbox property to true on it statically (you may also use a getter):
In order to make ensure that this property doesn't conflict with Objection or Knex internals, Schwifty.sandbox is a symbol.
An example
Consider two plugins, plugin-a and plugin-b,
the root server registers plugin-a.
plugin-a registers plugin-b.
plugin-a declares a knex instance by registering schwifty and the model Dogs using server.registerModel().
plugin-b declares the model Zombies using server.registerModel() and does not declare a knex instance.
Then we can say the following,
The root server will have access to both Dogs and Zombies models, having inherited both of them from its "children" plugins.
The root server has no knex instance of its own.
plugin-a will have access to both Dogs and Zombies models, having inherited Zombies from plugin-b.
plugin-b will have access to only the Zombies model.
Both Dogs and Zombies models will be bound to plugin-a's knex instance. Zombies is bound to that instance because plugin-b inherits its knex instance from its nearest "ancestor" with a knex instance, plugin-a.
What happens during server initialization?
Schwifty performs a few important routines during server initialization.
Binding models to knex
First, models are bound to instances of knex on a per-plugin basis. Each model is bound to the knex instance of the plugin—under plugin ownership of knex instances—in which the model was defined. If a model already is bound to a knex instance prior to initialization, it will not be bound to a new one. Additionally, if a model's Schwifty.bindKnex property is false then it will not be bound.
This means that prior to server initialization, calls to server.models() will provide models that will not be bound to a knex instance (unless you've done so manually). If you would like to perform some tasks during server initialization that rely on database-connected models, simply tell your onPreStart server extension to occur after schwifty, e.g.,
Second, every knex instance declared during plugin registration is checked for connectivity. If any instance of knex does not have database connectivity, you will receive an error and your server will not initialize. While this does not make any guarantees about table existence or structure, it does guarantee database connectivity at server initialization time.
Migrations
Lastly, if you specified migrateOnStart as true, 'latest', or 'rollback', then migrations will be run against each knex instance. Your instance of knex may specify its own knex migration options, except for directory, which will be ignored in favor of the migration directories declared using the migrationsDirplugin registration option.
If a knex instance is shared across plugins (under plugin ownership of knex instances) and each plugin specifies its own migrations directory using migrationsDir, then migrations from each of those plugin's migrations directories will simply be run against the knex instance. In short, schwifty pluginizes knex migrations.
Schwifty's model class extends Objection.Model, adding support for Joi schemas wherever objection's base model class employs jsonSchema. This primarily plays into model instance validation and serialization of JSON/array fields.
joiSchema
An optional Joi.object() schema, where each of its keys is a field of the given model.
joiSchemaPatch
This property is computed as a getter using the contents of joiSchema. It is identical to joiSchema, except it ignores default values and missing required fields. Objection additionally validates Model.query().patch() query input against this schema.
jsonAttributes
This property is computed as a getter using the contents of joiSchema. Any of the schema's keys that are Joi.object()s or Joi.array()s will be included in the list of JSON attributes. If this property is set, it will forget about having been computed. For more info, see objection's jsonAttributes.
field(name)
Returns the schema for the field named name on the model's joiSchema, but marked as optional and ignoring defaults. The schema also has two alterations: one named 'full' which respects defaults and required/optional/forbidden status, and another named 'patch' which does nothing but exists to allow for explicitness.
constUser=classUserextendsSchwifty.Model{staticjoiSchema=Joi.object({username: Joi.string().min(4).required()});};// no alterationUser.field('username').validate();// { value: undefined }User.field('username').validate('pal');// { error }, as username must be at least 4 charactersUser.field('username').validate('paldo');// { value: 'paldo' }// "patch" alteration: identical to the aboveUser.field('username').tailor('patch').validate();User.field('username').tailor('patch').validate('pal');User.field('username').tailor('patch').validate('paldo');// "full" alterationUser.field('username').tailor('full').validate();// { error }, as username is requiredUser.field('username').tailor('full').validate('pal');// { error }, as username must be at least 4 charactersUser.field('username').tailor('full').validate('paldo');// { value: 'paldo' }
model.$validate()
Validates the model instance using its joiSchema. This is implemented using objection's Validator interface.
Ensures that ModelA and ModelB have the same class name, share the same tableName, and that one model extends the other, otherwise throws an error. When message is provided, it will be used as the message for any thrown error.
Schwifty.migrationsStubPath
A path to a knex migrations stub file that utilizes async/await and is set to follow the hapi style guide. This is intended to be used with knexfile configurations (as of knex v0.18.4) and the knex migrate:make command:
A symbol that may be added as a property to a knex instance or a model in order to participate in sandboxing.
Sandboxing ensures that the object in question opts out of transitive ownership as described in "Plugin ownership of knex instances and models", and in turn is only visible within the plugin where it is provided.
When this property is set to true or 'plugin' the object will be sandboxed. The default behavior of plugin ownership can be explicitly configured using the value false or 'server'.
Schwifty.bindKnex
A symbol that may be added as a property to a model in order to opt-out of knex-binding during server initialization. When this property is set to false, a knex instance will not automatically be bound to the model.