domain-container
Version:
Tool to contain various domains (process domains), tied to the Krypton ORM
649 lines (479 loc) • 18.7 kB
Markdown
# DomainContainer Spec
## Index
- [Intro](#intro)
- [Feature summary](#feature-summary)
- [Background](#background)
- [Constraints](#constraints)
- [Assumptions](#assumptions)
- [Blackbox](#blackbox)
- [Functional spec](#functional-spec)
- [Technical spec](#technical-spec)
- [Code snippets](#code-snippets)
- [Examples](#examples)
- [Multi-site](#multi-site)
- [`customProps` use cases](#customprops-use-cases)
- [Mailers](#mailers)
## Intro
This document defines a standalone (not related to any particular project)
interface for Krypton models which offers a way to isolate the same models
according to different environments, stuff like multi-site setups where the data
structures are the same but the data is different since there are different DBs.
## Feature summary
- Allows you to give the same set of models different Knex instances
(especially useful in multi-site setups).
- Allows you to pass an arbitrary amount of properties to each model before any
actions are carried out (except when querying).
## Background
There is a need to have a container for the models and the environment that they
carry around, without having to put it all in the `req` variable of your routing
lib. I.e. not have to pass around a Knex instance in the `req`, mailer
instances, and many other environment specific vars.
This module means to solve that environment containment for the models and other
things the models may need.
As long as one has some sort of way to uniquely identify the different
DomainContainer, e.g. a subdomain, DomainContainer is useful as a way to
separate database environments.
## Constraints
- Has to use Neon DSL.
- Has to interface with Krypton ORM seamlessly.
- Has to provide a mechanism to pass in (once) an unspecified amount of
parameters to each model instance, which the instance may need to do its
thing.
## Assumptions
- The models that it handles are models generated by the Krypton ORM and they
have not been given a Knex instace, i.e. they are dynamic.
- All the used Krypton methods return promises.
- The environment it is used in will do the instantiation with its own logic
that applies to that environment.
- The environment knows how to store and access different DomainContainer
instances.
- The environment will implement some sort of caching mechanism, as it is not
cheap to be creating DomainContainer instances.
## Blackbox

- `._knex` is a Knex instance passed in at instantiation.
- `._modelExtras` is an object with things to pass to models before they
interact with the DB. Examples of usage are mailer instances or other such
things.
- `._models` is an object containing Krypton model constructors. The methods
reference this object in order to generate new models.
- `.props` is an object containing arbitrary, static properties of the
DomainContainer instance.
- `#query()` is a method that returns a QueryBuilder for the requested model
(which it grabs from `._models`) passing in the `._knex` instance.
- `#create()` is a method that creates the requested model (which it grabs from
`._models`) with the provided body, it saves to DB using `._knex` and passes
to the model the `._modelExtras` before doing `model.save()`.
- `#update()` is a method that updates the provided model with the provided
body, it saves to DB using `._knex` and passes to the model the
`._modelExtras` before doing `model.save()`.
- `#destroy()` is a method that destroys the provided model using `._knex`,
passes to the model the `._modelExtras` before doing `model.destroy()`.
- `#get()` is a method that returns a model class (which it grabs from
`._models` after assigning to it a Knex instance and the
`._modelExtras`. Useful for class methods.
- `#cleanup()`, destroys the Knex instance.
## Functional spec
The module will provide a Neon class called DomainContainer.
This class, at instantiation, will take on Knex instance, an object with Krypton
models, an optional presenters object and an optional modelExtras object.
These properties will be saved as instance variables (some with `_` prefix
to indicate they are private).
The DomainContainer instance will provide the following instance methods:
- `#query(modelName)` returns a Krypton model QueryBuilder from the model that
it is being requested.
- `#create(modelName, body)` creates a model instance with the properties of
`body` and saves the resulting model to the DB.
- `#update(modelInstance, body)` calls `.updateAttributes(body)` on the provided
model and saves the changes to the DB.
- `#destroy(modelInstance)` calls `.destroy()` on the provided model, destroying
the record in the DB.
- `#get(modelName)` returns a model class that has had a Knex and the
`modelExtras` applied to it, useful for class methods.
- `#cleanup()` destroys Knex instance.
Notes:
- All of the above methods use the Knex instance provided at the
DomainContainer's instantiation for their queries.
- All of the above methods that interact with the DB directly (`#create`,
`#update` and `#destroy`) pass in the DomainContainer instance's
`this._modelExtras` properties to the models so that the models may make use
of them. Also `#get`.
- All of the amove methods when not being passed the model (i.e. the `#query` and
`#create` methods) they grab the model by doing something like
`this._models[modelName]`.
- Most of the above methods (`#create`, `#update`, `#destroy` and
`#get`) add a `._container` variable to the model so the model can
make use of it internally and create models in context easily.
Observations:
- With the above setup, one could pass an instance that is generated by the
controller, i.e. the controller does:
```javascript
var model = new Model({ id: req.query.params });
req.container.destroy(model).then(...);
```
And that would properly destroy the model, since before being destroyed the
model instance is being passed in the `modelExtras`, so it can properly make
use of those variables.
## Technical spec
### Class `DomainContainer(<Object> initProps)`
`<Object> initProps` available properties:
- `<Knex> knex` a Knex instance which will be used in all the models' queries.
- `<Object> models` an object with all of the models the container will wrap.
Must be model constructors, not instances.
- `<Object> {Optional} modelExtras` props that will be handed to every model
instance that will be used to modify the DB in some way (not query).
- `<Object> {Optional} props` props that can serve as metadata for the
container, whatever you make of it.
- `<Object> {Optional} presenters` presenters for models.
#### Instance variables
These are mostly set by the `<Object> initProps` parameter set and
DomainContainer instantiation.
##### `_knex`
Required, no default.
Holds the Knex instance that will be provided to every model.
It's set to whatever the `initProps.knex` property was when instantiating the
DomainContainer.
##### `_models`
Required, no default.
Holds all the models that will be avaialble to the DomainContainer. Must be the
model constructors, not instances.
It's set to whatever the `initProps.models` property was when instantiating the
DomainContainer.
##### `_modelExtras`
Default: Empty object (`{}`).
Holds the properties that will be passed in to each model whenever it is being
instantiated to modify the DB, i.e. not being used to query.
This could be anything, changes from project to project, but an easy example
would be mailer instances which the models would use, instead of some global
one which isn't configured for the specific models.
It's set to whatever the `initProps.modelExtras` property was when instantiating
the DomainContainer.
##### `props`
Default: Empty object (`{}`).
Holds static properties about the DomainContainer, like a metadata variable
about the, like its ID within the system, or really whatever is useful for the
use case.
##### `presenters`
**NOTICE:** Presenters are currently not used, while how to define them is
defined at this document, they are not in use and thus the rest of the spec will
not define how to make use of them.
Default: Empty object (`{}`).
Presenters are functions that modify the model they are passed in in order to
make it consumeable by the front end, by removing sensitive data or adding more
data.
The object is made up of properties containing functions, the property name is
the name of the model. The function would be passed in a model instance which
it would manipulate, the function is expected to return a promise, if the
promise returns a value the model will be replaced by the returned value.
An example `presenters` object would be:
```javascript
{
User: function (user) {
delete user.password;
delete user.encryptedPassword;
delete user.token;
return Promise.resolve();
},
},
```
#### Instance methods
##### `#init(<Object> initProps)`
This method is run whenever the Class is instantiated, i.e. whenever a new
DomainContainer instance is created.
It should throw an error if `initProps.knex` or `initProps.models` are not
defined or if `initProps.models` is not an object.
Pseudo-code:
```text
if initProps.knex is undefined
throw error 'initProps.knex property is required'
else
set this._knex to be initProps.knex
if initProps.models is undefined or is not an object
throw error 'initProps.models property is required and should be an object'
else
set this._models to be initProps.models
extend this._modelExtras with initProps.modelExtras
extend this.props with initProps.props
extend this.presenters with initProps.presenters
```
##### `#query(<String> modelName)`
This method returns a Krypton QueryBuilder for the requested model. It returns
an error if the model doesn't exist.
Pseudo-code:
```text
let Model be that._models[modelName] // for convenience
if Model is undefined
return rejected promise with error 'Model '+modelName+' doesn\'t exist in the DomainContainer'
else return instance of Model's QueryBuilder with that._knex passed in
```
##### `#create(<String> modelName, <Object> body)`
This method creates a new entry in the DB with the model provided (identified by
`<String> modelName` parameter) and returns the instance of that model that was
saved. The contents of the new model are contained in the `<Object> body`
parameter. It returns an error if the model doesn't exist.
Pseudo-code:
```text
let Model be that._models[modelName] // for convenience
if Model is undefined
return rejected promise with error 'Model '+modelName+' doesn\'t exist in the DomainContainer'
make new instance of Model with the provided body
let model instance's ._modelExtras be this._modelExtras
let model instance's ._container point to this
return do model.save passing in the this._knex instance
then
return model instance
```
##### `#update(<Model> model, <Object> body)`
This method receives a pre-existing model and updates it with the parameters
passed in through the `<Object> body` parameter.
Pseudo-code:
```text
let passed in model's ._modelExtras to be this._modelExtras
let passed in model's ._container point to this
let body be an empty object by default
run model.updateAttributes with body parameter passed in
return do model.save passing in the this._knex instance
then
return model instance
```
##### `#destroy(<Model> model)`
This method destroys the record in the DB for the provided model (`<Model>
model` parameter).
Pseudo-code:
```text
let passed in model's ._modelExtras to be this._modelExtras
let passed in model's ._container point to this
return do model.destroy passing in the this._knex instance
then
return model instance
```
##### `#get(<String> modelName)`
This method returns a model class (which it grabs from `._models`
according ot the `<String> modelName>` parameter, after assigning to
it a Knex instance and the `._modelExtras`.
Designed mainly so one may make use of class methods in the models.
Pseudo-code:
```text
let Model be that._models[modelName] // for convenience
if Model is undefined
return rejected promise with error 'Model '+modelName+' doesn\'t exist in the DomainContainer'
create temp module which has in prototype
_modelExtras
_container
_knex
and as class static properties
_modelExtras
_container
create temp class which inherits from Model and includes temp module
call knex on temp class passing in this._knex
return temp class
```
##### `#cleanup()`
This method returns a promise. It simply runs `.destroy()` on the
Knex instance and it overrides all the methods with a method that
throws upon usage, so the DomainContainer can't be called anew.
Pseudo-code:
```text
run this._knex.destroy()
on the .destroy() callback/resolved promise return a resolved promise
```
## Code snippets
Let's quickly see what an instance looks like.
Input:
```javascript
var container = new DomainContainer({
knex: knex, // we got this from somewhere
models: {
User: User, // we got this from somewhere
},
modelExtras: {
mailers: mailers, // we got this from somewhere
},
});
```
Output:
```javascript
// this is an instance of DomainContainer
DomainContainer({
_knex: ..., // same as provided above
_models: ..., // same as provided above
_modelExtras: ..., // same as provided above, empty object default though
presenters: {}, // empty object default
props: {}, // empty object default
query: function () {...},
create: function () {...},
update: function () {...},
destroy: function () {...},
get: function () {...},
});
```
---
```javascript
var container = new DomainContainer({...});
container.query('User'); // => User QueryBuilder
```
---
```javascript
var container = new DomainContainer({...});
container
.create('User', {
email: 'example@gmail.com',
password: 'yay',
})
.then(function (user) {
// user is the model of the record that was created in the DB
});
// In the DB there is a new record:
// {
// id: 1,
// email: 'example@gmail.com',
// password: 'yay',
// }
//
// An email is sent from a mailer to example@gmail.com with a token
```
---
```javascript
var container = new DomainContainer({...});
var model = new User({ id: 1 });
container
.update(model, {
password: 'nope',
})
.then(function (user) {
// user is the model of the record that was created in the DB
});
// In the DB, the record created above is updated to:
// {
// id: 1,
// password: 'nope',
// ...
// }
//
// An email is sent from a mailer to example@gmail.com to inform of password change
```
---
```javascript
var container = new DomainContainer({...});
var model = new User({ id: 1 });
container
.destroy(model)
.then(function () {
// ...
});
// In the DB, the record created above no longer exists
```
---
To quickly demonstrate the use of several Knex instances:
```javascript
var container1 = new DomainContainer({
...
knex: knex1,
...
});
var container2 = new DomainContainer({
...
knex: knex2,
...
});
container1.query('User')
.where('id', 1)
.then(function (result) {
console.log(result);
// => {
// id: 1,
// email: 'first-container@example.com',
// ...
// }
});
container2.query('User')
.where('id', 1)
.then(function (result) {
console.log(result);
// => {
// id: 1,
// email: 'second-container@example.com',
// ...
// }
});
```
---
```javascript
var container = new DomainContainer({...});
container.get('User'); // => User model with ._knex and ._modelExtras set
```
---
Model making use of `._container`.
```javascript
var Model = Class({}, 'Model').inherits(Krypton.Model)({
prototype: {
init: function (config) {
var that = this;
that.on('beforeCreate', function (next) {
that._container
.create('Model2', { some: 'value' })
.then(function (model) {
return next();
})
.catch(next);
});
},
},
});
```
```javascript
var container = new DomainContainer({...});
container.cleanup(); // => Promise
```
## Examples
### Multi-site
The use case for which this module was designed goes as follows:
Setup:
- Requests can come in for `foo.domain.com` or `bar.domain.com`, `foo` and
`bar` subdomains have different databases, however their structures, and thus
models, are exactly the same.
- There is a `domain-parser` middleware, which sets the `req.subdomainName`
variable to `foo` or `bar` in this scenario.
- Right after there is a `domain-container` middleware, which has logic
somewhat like the following:
```text
// Pseudo-code
// Outside middleware context:
let containers be an empty object
// Inside middleware context:
function (req, res, next)
let name be req.subdomainName;
var newContainer;
if you can find current container (name) in containers
let req.container be what we found
else
create new container
add new container to containers
let req.container be to the new container
call next
```
General flow:
- A request comes in for `foo.domain.com`
- `domain-parser` middleware sets `req.subdomainName` to `foo`
- `domain-container` middleware finds no cache for a `foo` container, sets
`req.container` to a new instance of DomainContainer
- Controller somewhere down the line does `req.container.query('User')...`
What the above effectively achieves is to have one configuration of models for
`foo` subdomain and another for `bar` subdomain.
### `modelExtras` use cases
#### Mailers
Let's say you've a multi-site setup, so you must know the current URL being used
in order to send your emails in your mailers, simply create one instance of the
mailer per site and the instance should keep track of the URL.
OK, but the models want to use the mailers, they can't do
`UserMailer.sendEmail()` just like that, because they don't have the context of
the mailer's instance, unless you put the mailer in the `req` and give the model
the `req`, but we want to avoid that.
So we can give the DomainContainer instance the mailer instances through the
`modelExtras` property, as `modelExtras.mailers` or something like that.
The DomainContainer then will assign `modelExtras` to each model when it
instantiates it itself or when it's handling a model it is given, so that the
model has the contextualized mailers available to it.
The models can then use the mailers as
`this._modelExtras.mailers.user.sendEmail()` and the email will be sent with the
proper context of the current domain.