# 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

![blackbox image on imgur](https://www.lucidchart.com/publicSegments/view/41711772-da29-4522-a9c0-728c4ce242de/image.png)

- `._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.
