hapipal/boilerplate

migrations location

jamesdixon opened this issue · 12 comments

Hi again! Was your intention for the migrations directory to live under lib/?

Hey– good question! I understand how it may seem odd, but yes. Schwifty pluginifies migrations– different plugins can have different migration directories. Schwifty will pull all your migrations (across multiple plugins' migration folders) into a single temp directory in order to run them together during server initialization. Since they're specific to the plugin, I put them in lib/. Open to feedback on this– clearly it caused a little bit of confusion, especially in the absence of documentation.

All good! I know that you've thought this stuff out, so just trying to wrap my head around it. This is a side project for me, but my existing Hapi project is a mish-mash of things that I learned "on the job," so this type of stuff is really helpful to get organized!

Question: what's a case where you've/would use per-plugin migrations?

Guessing a simple use case would be the underlying table to support a model?

The example we go back to a lot is a project of @mattboutet's which started as a fork of this boilerplate that also has models/routes/auth to support "Users": https://github.com/mattboutet/user-boilerplate

Now, rather than the user boilerplate being used as a boilerplate (which got in the way of code reuse and maintenance), we want to incorporate it into our apps by simply registering it as a plugin. In order for that to work, each plugin will need to have its own models and its own migrations to support those models.

The goal is to be able to create a large application as separate plugins that may be deployed either together or separately– to that end, each of those plugins will have their own models, migrations, and possibly their own DB connections. Schwifty is designed to support that. If you've run into server.models(true) vs. server.models(false) yet with schwifty, that's a result of us being really explicit about "plugins owning models" and making it clear when one plugin is reaching into another's models– something that's important to be able to do, but we think should happen explicitly.

Getting off topic here, but relatedly we also have patterns (written down, to be shared soon) for, e.g. extending a user plugin's User model with application-specific columns, etc. This way we can make general purpose plugins that have their own routes, models, etc.

Thanks for the great explanation. Plugins owning models is something I've been curious about. Is this something you recommend even when that a particular set of model/route/auth is only local to a specific application? The boilerplate appears to be set up for divvying those things up, but I've also read to pluginify everything!

Welcome! :) 🍻

Making sure I understand– are you wondering about plugins owning their own models versus having a single plugin specifically for providing all models?

Correct!

For example:

Approach 1
Everything lives inside the plugin for a given entity.

- lib/
| -- plugins/
| -- | -- user/
| -- | -- | -- migrations.js
| -- | -- | -- routes.js
| -- | -- | -- model.js
| -- | -- | -- handlerjs
| -- | -- | -- index.js

Approach 2
An entity is broken up into pieces and placed in the corresponding folder under the lib plugin.

- lib/
| -- handlers/
| -- | -- user.js
| -- models/
| -- | -- user.js
| -- routes/
| -- | -- user.js
| -- migrations/
| -- | -- user.js

Outside of potentially re-using the plugins elsewhere, is there one approach that stands out to you? Pros/cons? Situations where you may use one vs. another?

Cheers!

Also, sorry for all of the questions. I really like the boilerplate and I believe things like these are somewhat confusing to Hapi newcomers, so I figured it'd be worth bringing up 👍

Ah, gotchya. First, no worries– I enjoy answering and thinking about these questions when I have time, plus I'm often supported by my employer to do it! I hope/plan that issues like this will be converted into documentation over time. I've alluded to this before, but we also have some best practices articles (covering topics exactly like this!) that we'll be putting out before too long.

Regarding pluginizing different areas of your app– I think there are a few ways to slice it. Generally pluginizing code should either be useful to your deployment or your developer-brain (and hopefully both!). Pluginizing is useful for your deployment when you find an area of your application that might become its own service some day– in that case, it's very cheap to create a plugin boundary around it, knowing that it will not be much more work to eventually deploy that plugin on its own. Pluginizing is useful for your developer-brain if it makes it simpler to add features and fix bugs.

In either case, when you create a plugin boundary, you don't want to be creating cross-plugin dependencies– that both makes it harder to deploy a plugin outside of the context of other plugins (bad for deployment) and also harder to develop features/bugs because they will span multiple plugins (bad for your developer-brain). In other words, cross-plugin dependencies count against the benefits of pluginization. Not that there can't be dependencies! But generally you want to avoid introducing them when possible.

So my advice would be to identify areas of your application that are relatively independent of the rest of your application, and carve those out into plugins; pluginify only so far as you can without introducing lots of plugin dependencies.

Getting a step more concrete, with your user example above... organizing plugins around single entities can definitely be fine, but more often then not I think you'll want to find a natural grouping of entities that look more like a "service" that might be useful on its own. For example, if you have users, you might also have things like sessions, auth-tokens, user-settings, groups, acls, etc. It probably makes sense to identify the nexus of those entities as a hot-spot of related bug- and feature-development, or what might be a cohesive, useful standalone service someday, and make a single plugin that governs all of those entities.

That's about all I have to offer on that for now– curious of your thoughts/experience!

P.S. I definitely like the idea you've alluded to above of using the lib/plugins/ folder as a place to break-out other (potentially haute-couture-oriented) plugins. Originally I imagined it as being a place where plugins would be registered, but not necessarily authored. It definitely could become a good practice to author little "proto-services" or other cross-cutting concerns (e.g. a plugin for app-specific error transformation) in there.

Thanks again for such detailed replies! This is super helpful and makes a lot of sense.

My previous approach has been much of what you described. However, in some cases, I have used the approach of organizing single entities into plugins. For example, I have some cases where I wanted to massage incoming/outgoing data. I stuffed this into a plugin using onPreResponse. There is probably a better way, but nonetheless, it was an experiment that I've tried. I'm still trying to wrap my head around all that Hapi has to offer and attempting learn best practices in the process!

Regarding using /lib/plugins for registering plugins, is it really needed given that plugins can also be registered in manifest.js? Pros/cons?

Ah, I see– that makes sense! Some plugins are application-oriented (with things like models, routes), and others are for cross-cutting concerns (like error transformation). Wish we had a name for those two different types of plugins!

Bottom like for me regarding the manifest vs lib/plugins/ is this: if your plugin (in lib/) depends on other plugins, it should register them itself in lib/plugins/. Beyond that, it's sort of up to you! Our swagger-ui flavor doesn't affect your plugin– just the deployment via the manifest. Because your API probably doesn't need to rely on swagger– it's enriched by swagger via its deployment. Also, if you imagine deploying two API plugins next to each other and they each try to register hapi-swagger, that's going to cause an error.