UNPKG

domain-container

Version:

Tool to contain various domains (process domains), tied to the Krypton ORM

649 lines (479 loc) 18.7 kB
# 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.