diff --git a/.gitignore b/.gitignore index dff1457..15fcd95 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -npm-debug.log -node_modules -package-lock.json -.nyc_output +npm-debug.log +node_modules +package-lock.json +.nyc_output diff --git a/LICENCE b/LICENCE index 1f6aa19..818e54a 100644 --- a/LICENCE +++ b/LICENCE @@ -1,20 +1,20 @@ -The MIT License (MIT) -Copyright (c) 2018 Erik Landvall - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +The MIT License (MIT) +Copyright (c) 2018 Erik Landvall + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index acf6d5b..3eb852f 100644 --- a/README.md +++ b/README.md @@ -1,976 +1,976 @@ -# Core - -Licence: [MIT](https://opensource.org/licenses/MIT) - ---- - -[![npm version](https://badge.fury.io/js/superhero.svg)](https://badge.fury.io/js/superhero) - -- A core framework I use when developing in [nodejs](https://nodejs.org/en/docs/). -- I built the framework to help me build applications after a reactive [domain driven design (DDD)](https://en.wikipedia.org/wiki/Domain-driven_design) approch. -- The framework is designed to have little to no production dependencies. -- The framework offers solutions for different topics, not necessarily an http server. -- The vision of the framework is to offer a code structure to the developer that will help segregate responsibilities in projects through a [SOLID](https://en.wikipedia.org/wiki/SOLID) [OOP](https://en.wikipedia.org/wiki/Object-oriented_programming) approach. - -## Addons - -Consider extending the framework with one or multiple plugins where the extra functionality is required: -*Please note; you should use matching versions for core and the plugins by major and future releases* - -- [core-handlebars](http://github.com/superhero/js.core.handlebars) -- [core-resource](http://github.com/superhero/js.core.resource) -- [core-websocket](http://github.com/superhero/js.core.websocket) - -## Install - -`npm install superhero` - -...or just set the dependency in your `package.json` file: - -```json -{ - "dependencies": - { - "superhero": "*" - } -} -``` - -## Standards - -Recommended standards are published in this repository, stored in the [`doc/standard`](doc/standard) folder. - -***OBS!*** These standards are still being developed, rapid evolution between version 1 and 2 is to be expected. - - - [Microservice (sop-ms)](doc/standard/sop-ms.md) - - [Source code (sop-src)](doc/standard/sop-src.md) - - [API (sop-api)](doc/standard/sop-api.md) - - [Domain (sop-domain)](doc/standard/sop-domain.md) - - [Infrastructure (sop-infrastructure)](doc/standard/sop-infrastructure.md) - - [View (sop-view)](doc/standard/sop-view.md) - - [Test (sop-test)](doc/standard/sop-test.md) - - [Event (sop-event)](doc/standard/sop-event.md) - -## Automatic code and documentation generation - -The package comes with a **CLI** helper (**C**ommand **L**ine **I**nterface). You do not need to use this helper in order to use the framework, but if you do get used to the helper you should be able to speed up your productivity. - -If you install the package globally, by running the folowing command `npm install superhero -g`, then you can use a CLI helper, that can be accessed from the terminal by running the command `superhero`: - -``` - __ - _______ ______ ___ _____/ /_ ___ _________ - / ___/ / / / __ \/ _ \/ ___/ __ \/ _ \/ ___/ __ \ - (__ ) /_/ / /_/ / __/ / / / / / __/ / / /_/ / - /____/\__,_/ .___/\___/_/ /_/ /_/\___/_/ \____/ - /_/ - - -1. Generate project -2. Generate API classes and corresponding tests from configuration -3. Generate API documentation -4. Generate Domain classes from configuration - -Choose your destiny: -``` - -As you can see, there's a menu where you can select different aid for code and documentation generation. - -### Problems installing globally? - -This is a guide on how to install the package globally, if you have problems with permissions, eg: `npm WARN checkPermissions Missing write access to /usr/lib/node_modules` - -#### Solution - -Tested on a debian distribution. Run the following commands: - -``` -mkdir ~/.npm-global -npm config set prefix '~/.npm-global' -``` - -Open or create a `~/.profile` file and add this line: - -``` -export PATH=~/.npm-global/bin:$PATH -``` - -Update your system variables by running: - -``` -source ~/.profile -``` - -Then again, run `npm install superhero -g` - -## Example application - -An example application to get started, covering some recommended "best practices" regarding testing and docs generation. - -The application is a calculator with very basic functionality. The aim with this application is not to make a good calculator. The aim is to show you, who is reading this, how different parts of the framework works, or are intended to work. - -### Example Application › File structure - -``` -super-duper-app -├── docs -├── src -│ ├── api -│ │ ├── endpoint -│ │ │ ├── append-calculation.js -│ │ │ └── create-calculation.js -│ │ ├── middleware -│ │ │ └── authentication.js -│ │ ├── observer -│ │ │ ├── calculation-appended -│ │ │ │ └── log -│ │ │ │ ├── index.js -│ │ │ │ └── locator.js -│ │ │ └── calculation-created -│ │ │ └── log -│ │ │ ├── index.js -│ │ │ └── locator.js -│ │ └── config.js -│ ├── domain -│ │ ├── aggregate -│ │ │ └── calculator -│ │ │ ├── error -│ │ │ │ ├── calculation-could-not-be-found.js -│ │ │ │ └── invalid-calculation-type.js -│ │ │ ├── index.js -│ │ │ └── locator.js -│ │ ├── schema -│ │ │ └── entity -│ │ │ └── calculation.js -│ │ └── config.js -│ └── index.js -├── test -│ ├── integration -│ │ ├── test.calculations.js -│ │ └── test.logger.js -│ ├── init.js -│ └── mocha.opts -├── .gitignore -├── package.json -└── README.md -``` - -The file structure is here divided to 2 different modules - -- `api` *- responsible for the endpoints and observers.* -- `domain` *- responsible for domain logic.* - -#### `.gitignore` - -``` -docs/generated -npm-debug.log -node_modules -package-lock.json -.nyc_output -``` - -Set up a `.gitignore` file to ignore some auto-generated files to keep a clean repository. - -#### `package.json` - -```js -{ - "name": "Super Duper App", - "version": "0.0.1", - "description": "An example application of the superhero framework", - "repository": "https://github.com/...", - "license": "MIT", - "main": "src/index.js", - "author": { - "name": "Padawan", - "email": "padawan@example.com" - }, - "scripts": { - "docs-coverage": "nyc mocha './{,!(node_modules)/**}/test.*.js' --opts ./test/mocha.opts && nyc report --reporter=html --report-dir=./docs/generated/coverage", - "docs-tests": "mocha './{,!(node_modules)/**}/test.*.js' --opts ./test/mocha.opts --reporter mochawesome --reporter-options reportDir=docs/generated/test,reportFilename=index,showHooks=always", - "test": "nyc mocha './{,!(node_modules)/**}/test.*.js' --opts ./test/mocha.opts", - "start": "node ./src/index.js" - }, - "dependencies": { - "superhero": "*" - }, - "devDependencies": { - "mocha": "5.1.0", - "mochawesome": "3.0.2", - "chai": "4.1.2", - "nyc": "11.7.1" - } -} -``` - -Our [`package.json`](https://docs.npmjs.com/files/package.json) file will dictate what dependencies we use. This example application will go through test cases, why we have a few `devDependencies` defined. - -#### `index.js` - -```js -const -CoreFactory = require('superhero/core/factory'), -coreFactory = new CoreFactory, -core = coreFactory.create() - -core.add('api') -core.add('domain') - -core.load() - -core.locate('core/bootstrap').bootstrap().then(() => -core.locate('core/http/server').listen(process.env.HTTP_PORT)) -``` - -We start of by creating a core factory that will create the core object, central to our application. The core is designed to keep track of a global state related to it's context. You can create multiple cores if necessary, but normally it makes sens to only use one. This example will only use one core. - -The next step is to add services that we will use in relation to the core context. Here we add `api` and `domain` that exists in the file tree previously defined. You also notice that we add the service `core/http/server` that does not exist in the file tree we defined. The `core/http/server` service is located in the core, but may not always be necessary to load depending on the scope of your application, so you need to add the `core/http/server` service when you want to set up an http server. -The framework will add services depending on a hierarchy of paths, it's possible to write over the configuration done by one component in another. This feature allows the developer to extend, or build on to, components declared functionality. - -- First it will try to load in relation to your `main script` -- next it attempts by dependency defined in your `package.json` file -- and finally it will attempt to load related to the core library of existing services. - -*This hierarchy allows you, as a developer, to overwrite anything in the framework with custom logic.* - -Next we load the core! This will eager load all the services previously added to the context. - -By bootstrapping the core, we run a few scripts that needs to run for some modules to function properly. As a developer you can hook into this process in the modules you write. - -And in the end, after we have added, loaded and bootstrapped the modules related to the context, we tell the server to listen to a specific port for network activity. In this case, the environment variable `HTTP_PORT``is expected to be set to an integer. - -### Api - -#### `src/api/config.js` - -```js -/** - * @namespace Api - */ -module.exports = -{ - core: - { - http: - { - server: - { - routes: - { - 'create-calculation': - { - url : '/calculations', - method : 'post', - endpoint: 'api/endpoint/create-calculation', - view : 'core/http/server/view/json', - input : false - }, - 'authentication': - { - middleware : - [ - 'api/middleware/authentication' - ] - }, - 'append-calculation': - { - url : '/calculations/:id', - method : 'put', - endpoint: 'api/endpoint/append-calculation', - view : 'core/http/server/view/json', - input : 'entity/calculation' - } - } - } - }, - eventbus: - { - observers: - { - 'calculation created' : { 'api/observer/calculation-created/log' : true }, - 'calculation appended' : { 'api/observer/calculation-appended/log' : true } - } - }, - locator: - { - 'api/observer/calculation-created/log' : __dirname + '/observer/calculation-created/log', - 'api/observer/calculation-appended/log' : __dirname + '/observer/calculation-appended/log' - } - } -} - -``` - -I have here chosen to set up a folder structure with a module called `api`. This module will contain all the endpoints. As such, the config file of this module will specify the router setting for these endpoints. - -I set up two explicit routes: `create-calculation` and `append-calculation`, and one middleware route: `authentication`. - -- The **url** attribute declares what **url pathname** the route will be valid for. -- The **method** attribute declares what **method** the route will be valid for - -The middleware route does not have an action or method constraint specified, so it's considered valid, but it does not have an endpoint specified; declaring it's unterminated. When a route is valid, but unterminated, then it will be merged together with every other valid route specified until one that is terminated appears, eg: one that has declared an endpoint. - -The `api` in this example is also responsible for attaching event listeners to events, or "domain events". In this example, we attach a logger observer to specific events. Notice that the `locator` describes where the observers can be located, then used in the `core.eventbus.observers` config context. - -#### `src/api/endpoint/create-calculation.js` - -```js -const Dispatcher = require('superhero/core/http/server/dispatcher') - -/** - * @memberof Api - * @extends {superhero/core/http/server/dispatcher} - */ -class CreateCalculationEndpoint extends Dispatcher -{ - dispatch() - { - const - calculator = this.locator.locate('domain/aggregate/calculator'), - calculationId = calculator.createCalculation() - - this.view.body.id = calculationId - } -} - -module.exports = CreateCalculationEndpoint -``` - -The `create calculation` endpoint is here defined. - -- First we locate the `calculator` service. -- Next we create a calculation -- And finally we populate the `view model` with the returning calculation id - -***OBS! it's possible to define the `dispatch` method as `async`. The framework will `await` for the method to finish*** - -#### `src/api/endpoint/append-calculation.js` - -```js -const -Dispatcher = require('superhero/core/http/server/dispatcher'), -NotFoundError = require('superhero/core/http/server/dispatcher/error/not-found'), -BadRequestError = require('superhero/core/http/server/dispatcher/error/bad-request') - - -/** - * @memberof Api - * @extends {superhero/core/http/server/dispatcher} - */ -class AppendCalculationEndpoint extends Dispatcher -{ - dispatch() - { - const - calculator = this.locator.locate('domain/aggregate/calculator'), - calculation = this.route.dto, - result = calculator.appendToCalculation(calculation) - - this.view.body.result = result - } - - onError(error) - { - switch(error) - { - case 'E_CALCULATION_COULD_NOT_BE_FOUND': - throw new NotFoundError('Calculation could not be found') - - case 'E_INVALID_CALCULATION_TYPE': - throw new BadRequestError(`Unrecognized type: "${this.route.dto.type}"`) - - default: - throw error - } - } -} - -module.exports = AppendCalculationEndpoint -``` - -The `append calculation` endpoint is here defined. - -- We start by showing you how to access data in the request through the `dto` *(Data Transfer Object)* located on the route. -- Next we locate the `calculator` service. -- Followed by appending a calculation to the stored calculated sum. -- And finally we populate the `view model` with the result of the calculation. - -Apart from the `dispatch` method, this time, we also define an `onError` method. This is the method that will be called if an error is thrown in the `dispatch` method. The first parameter to the `onError` method is the error that was thrown in the `dispatch` method. - -#### `src/api/middleware/authentication.js` - -```js -const -Dispatcher = require('superhero/core/http/server/dispatcher'), -Unauthorized = require('superhero/core/http/server/dispatcher/error/unauthorized') - -/** - * @memberof Api - * @extends {superhero/core/http/server/dispatcher} - */ -class AuthenticationMiddleware extends Dispatcher -{ - async dispatch(next) - { - const - configuration = this.locator.locate('core/configuration'), - apikey = this.request.headers['api-key'] - - if(apikey === 'ABC123456789') - { - await next() - } - else - { - throw new Unauthorized('You are not authorized to access the requested resource') - } - } -} - -module.exports = AuthenticationMiddleware -``` - -This middleware is used for authentication. It's a simple implementation, one should look at using a more robust solution, involving an ACL, when creating a solution for production environment. This example serves a purpose to show a good use-case for when to apply a middleware, not how best practice regarding authentication is defined. - -**OBS!** The post handling (after the `next` callback has been called) will be handled in reversed order. - -``` - Middleware - ↓ ↑ - Middleware - ↓ ↑ - Endpoint -``` - -### Api - Observer - -#### `src/api/observer/calculation-appended/log/index.js` - -```js -/** - * @memberof Api - * @implements {superhero/core/eventbus/observer} - */ -class ObserverCalculationAppendedLog -{ - constructor(console, eventbus) - { - this.console = console - this.eventbus = eventbus - } - - observe(data) - { - this.console.log(data) - this.eventbus.emit('logged calculation appended event', data) - } -} - -module.exports = ObserverCalculationAppendedLog -``` - -*See description below...* - -#### `src/api/observer/calculation-appended/log/locator.js` - -```js -const -ObserverCalculationAppendedLog = require('.'), -LocatorConstituent = require('superhero/core/locator/constituent') - -/** - * @memberof Api - * @extends {superhero/core/locator/constituent} - */ -class ObserverCalculationAppendedLogLocator extends LocatorConstituent -{ - /** - * @returns {ObserverCalculationAppendedLog} - */ - locate() - { - const - console = this.locator.locate('core/console'), - eventbus = this.locator.locate('core/eventbus') - - return new ObserverCalculationAppendedLog(console, eventbus) - } -} - -module.exports = ObserverCalculationAppendedLogLocator -``` - -*See description below...* - -#### `src/api/observer/calculation-created/log/index.js` - -```js -/** - * @memberof Api - * @implements {superhero/core/eventbus/observer} - */ -class ObserverCalculationCreatedLog -{ - constructor(console, eventbus) - { - this.console = console - this.eventbus = eventbus - } - - observe(data) - { - this.console.log(data) - this.eventbus.emit('logged calculation created event', data) - } -} - -module.exports = ObserverCalculationCreatedLog -``` - -*See description below...* - -#### `src/api/observer/calculation-created/log/locator.js` - -```js -const -ObserverCalculationCreatedLog = require('.'), -LocatorConstituent = require('superhero/core/locator/constituent') - -/** - * @memberof Api - * @extends {superhero/core/locator/constituent} - */ -class ObserverCalculationCreatedLogLocator extends LocatorConstituent -{ - /** - * @returns {ObserverCalculationCreatedLog} - */ - locate() - { - const - console = this.locator.locate('core/console'), - eventbus = this.locator.locate('core/eventbus') - - return new ObserverCalculationCreatedLog(console, eventbus) - } -} - -module.exports = ObserverCalculationCreatedLogLocator -``` - -The logger observers simply implements an interface to be recognized as an observer by the `eventbus`. - -After we have logged the event to the console, we emit an event to broadcast that we have logged to the console. When broadcasting an event, we can observe this event in the future, if we like. For instance, when you like to create a test for the method, we can listen to this event to ensure the process is being fulfilled. - -### Domain - -#### `src/domain/aggregate/calculator/index.js` - -```js -const -CalculationCouldNotBeFoundError = require('./error/calculation-could-not-be-found'), -InvalidCalculationTypeError = require('./error/invalid-calculation-type') - -/** - * Calculator service, manages calculations - * @memberof Domain - */ -class AggregateCalculator -{ - /** - * @param {superhero/core/eventbus} eventbus - */ - constructor(eventbus) - { - this.eventbus = eventbus - this.calculations = [] - } - - /** - * @returns {number} the id of the created calculation - */ - createCalculation() - { - const id = this.calculations.push(0) - this.eventbus.emit('calculation created', { id }) - return id - } - - /** - * @throws {E_CALCULATION_COULD_NOT_BE_FOUND} - * @throws {E_INVALID_CALCULATION_TYPE} - * - * @param {CalculatorCalculation} dto - * - * @returns {number} the result of the calculation - */ - appendToCalculation({ id, type, value }) - { - if(id < 1 - || id > this.calculations.length) - { - throw new CalculationCouldNotBeFoundError(`ID out of range: "${id}/${this.calculations.length}"`) - } - - switch(type) - { - case 'addition': - { - const result = this.calculations[id - 1] += value - this.eventbus.emit('calculation appended', { id, type, result }) - return result - } - case 'subtraction': - { - const result = this.calculations[id - 1] -= value - this.eventbus.emit('calculation appended', { id, type, result }) - return result - } - default: - { - throw new InvalidCalculationTypeError(`Unrecognized type used for calculation: "${type}"`) - } - } - } -} - -module.exports = AggregateCalculator -``` - -The calculator aggregate is here defined. Good practice dictate that we should define isolated errors for everything that can go wrong. First we require and define these errors to have access to the type and to be able to trow them when needed. - -It's a simple aggregate that creates a calculation and allows to append an additional calculation to an already created calculation. - -#### `src/domain/aggregate/calculator/locator.js` - -```js -const -Calculator = require('.'), -LocatorConstituent = require('superhero/core/locator/constituent') - -/** - * @memberof Domain - * @extends {superhero/core/locator/constituent} - */ -class AggregateCalculatorLocator extends LocatorConstituent -{ - /** - * @returns {AggregateCalculator} - */ - locate() - { - const eventbus = this.locator.locate('core/eventbus') - return new AggregateCalculator(eventbus) - } -} - -module.exports = AggregateCalculatorLocator -``` - -The locator is responsible for dependency injection related to the aggregates it factor. - -#### `src/domain/aggregate/calculator/error/calculation-could-not-be-found.js` - -```js -/** - * @memberof Domain - * @extends {Error} - */ -class CalculationCouldNotBeFoundError extends Error -{ - constructor(...args) - { - super(...args) - this.code = 'E_CALCULATION_COULD_NOT_BE_FOUND' - } -} - -module.exports = CalculationCouldNotBeFoundError -``` - -A specific error with a specific error code; specifying what specific type of error is thrown. - -#### `src/calculator/error/invalid-calculation-type.js` - -```js -/** - * @memberof Domain - * @extends {Error} - */ -class InvalidCalculationTypeError extends Error -{ - constructor(...args) - { - super(...args) - this.code = 'E_INVALID_CALCULATION_TYPE' - } -} - -module.exports = InvalidCalculationTypeError -``` - -Another specific error... - -#### `src/domain/schema/entity/calculation.js` - -```js -/** - * @memberof Domain - * @typedef {Object} EntityCalculation - * @property {number} id - * @property {string} type - * @property {number} value - */ -const entity = -{ - 'id': - { - 'type' : 'schema', - 'schema' : 'value-object/id', - 'trait' : 'id' - }, - 'type': - { - 'type': 'string', - 'enum': - [ - 'addition', - 'subtraction' - ] - }, - 'value': - { - 'type': 'decimal' - } -} - -module.exports = entity -``` - -#### `src/domain/schema/value-object/id.js` - -```js -/** - * @memberof Domain - * @typedef {Object} ValueObjectId - * @property {number} id - */ -const valueObject = -{ - 'id': - { - 'type' : 'integer', - 'unsigned': true - } -} - -module.exports = valueObject -``` - -Defining a JSON schema for an entity; calculation. It's a good praxis to also define the type in "jsdoc", as seen above. - -A table over validation and filtration rules follows... - -| | csv | boolean | decimal | integer | json | schema | string | timestamp | -|----------------------|-----------|-----------|-----------|-----------|-----------|-----------|-----------|-----------| -| default | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | -| collection | boolean | boolean | boolean | boolean | boolean | boolean | boolean | boolean | -| collection-size-min | number | number | number | number | number | number | number | number | -| collection-size-max | number | number | number | number | number | number | number | number | -| optional | boolean | boolean | boolean | boolean | boolean | boolean | boolean | boolean | -| nullable | boolean | boolean | boolean | boolean | boolean | boolean | boolean | boolean | -| unsigned | ✗ | ✗ | boolean | boolean | ✗ | ✗ | ✗ | ✗ | -| min | number | ✗ | number | number | ✗ | ✗ | number | timestamp | -| max | number | ✗ | number | number | ✗ | ✗ | number | timestamp | -| gt | number | ✗ | number | number | ✗ | ✗ | number | timestamp | -| lt | number | ✗ | number | number | ✗ | ✗ | number | timestamp | -| length | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | number | ✗ | -| enum | array | array | array | array | ✗ | ✗ | array | array | -| uppercase | boolean | ✗ | ✗ | ✗ | ✗ | ✗ | boolean | ✗ | -| lowercase | boolean | ✗ | ✗ | ✗ | ✗ | ✗ | boolean | ✗ | -| not-empty | boolean | ✗ | ✗ | ✗ | ✗ | ✗ | boolean | ✗ | -| stringified | ✗ | ✗ | ✗ | ✗ | boolean | ✗ | ✗ | ✗ | -| indentation | ✗ | ✗ | ✗ | ✗ | number | ✗ | ✗ | ✗ | -| schema | ✗ | ✗ | ✗ | ✗ | ✗ | string | ✗ | ✗ | -| trait | ✗ | ✗ | ✗ | ✗ | ✗ | string | ✗ | ✗ | - -#### `src/domain/config.js` - -```js -/** - * @namespace Domain - */ -module.exports = -{ - core: - { - schema: - { - composer: - { - 'entity/calculation' : __dirname + '/schema/entity/calculation' - } - }, - locator: - { - 'domain/aggregate/calculator' : __dirname + '/aggregate/calculator' - } - } -} -``` - -In the `domain` `config` we define where a path to a module is located, and where the `locator` can find a constituent `locator` to locate/find a service through. - -### Test - -#### `test/mocha.opts` - -``` ---require test/init.js ---ui bdd ---full-trace ---timeout 5000 -``` - -There will probably be a lot of [settings you need to set for mocha](https://mochajs.org/api/mocha), sooner or later; just as well that we make it a praxis to define the options outside your `package.json` file. - -#### `test/init.js` - -```js -require.main.filename = __dirname + '/../src/index.js' -require.main.dirname = __dirname + '/../src' -``` - -The init script must set some variables for the core to function as expected in testing. - -The port is by design set through the environment variable `HTTP_PORT`. While testing we can set the variable to what ever we like, and what ever is suitable for the local machine we are on. - -#### `test/test.calculations.js` - -```js -describe('Calculations', () => -{ - const - expect = require('chai').expect, - context = require('mochawesome/addContext') - - let core - - before((done) => - { - const - CoreFactory = require('superhero/core/factory'), - coreFactory = new CoreFactory - - core = coreFactory.create() - - core.add('domain') - core.add('api') - - core.load() - - core.locate('core/bootstrap').bootstrap().then(() => - { - core.locate('core/http/server').listen(9001) - core.locate('core/http/server').onListening(done) - }) - }) - - after(() => - { - core.locate('core/http/server').close() - }) - - it('A client can create a calculation', async function() - { - const configuration = core.locate('core/configuration') - const httpRequest = core.locate('core/http/request') - context(this, { title:'route', value:configuration.find('core.http.server.routes.create-calculation') }) - const response = await httpRequest.post('http://localhost:9001/calculations') - expect(response.data.id).to.be.equal(1) - }) - - it('A client can append a calculation to the result of a former calculation if authentication Api-Key', async function() - { - const configuration = core.locate('core/configuration') - const httpRequest = core.locate('core/http/request') - context(this, { title:'route', value:configuration.find('core.http.server.routes.append-calculation') }) - const url = 'http://localhost:9001/calculations/1' - const data = { id:1, type:'addition', value:100 } - const response_unauthorized = await httpRequest.put({ url, data }) - expect(response_unauthorized.status).to.be.equal(401) - const headers = { 'Api-Key':'ABC123456789' } - const response_authorized = await httpRequest.put({ headers, url, data }) - expect(response_authorized.data.result).to.be.equal(data.value) - }) -}) -``` - -#### `test/test.logger.js` - -```js -describe('Logger', () => -{ - const - expect = require('chai').expect, - context = require('mochawesome/addContext') - - let core - - before((done) => - { - const - CoreFactory = require('superhero/core/factory'), - coreFactory = new CoreFactory - - core = coreFactory.create() - - core.add('domain') - core.add('api') - - core.load() - - core.locate('core/bootstrap').bootstrap().then(done) - }) - - it('the logger is logging', function(done) - { - const configuration = core.locate('core/configuration') - const eventbus = core.locate('core/eventbus') - context(this, { title:'observers', value:configuration.find('core.http.eventbus.observers') }) - eventbus.once('logged calculation created event', () => done()) - eventbus.emit('calculation created', 'test') - }) -}) -``` - -Finally I have designed a few simple tests that proves the pattern, and gives insight to the expected interface. Here I suggest using a [BDD](https://en.wikipedia.org/wiki/Behavior-driven_development) approach, though my examples are not aligned with the principles. - -### Npm scripts - -#### To run the application: - -``` -npm install --production -npm start -``` - -#### To test the application: - -``` -npm install -npm test -``` - -#### For an auto-generated coverage report in html: - -``` -npm run docs-coverage -``` - -#### For an auto-generated test report in html: - -``` -npm run docs-tests -``` +# Core + +Licence: [MIT](https://opensource.org/licenses/MIT) + +--- + +[![npm version](https://badge.fury.io/js/superhero.svg)](https://badge.fury.io/js/superhero) + +- A core framework I use when developing in [nodejs](https://nodejs.org/en/docs/). +- I built the framework to help me build applications after a reactive [domain driven design (DDD)](https://en.wikipedia.org/wiki/Domain-driven_design) approch. +- The framework is designed to have little to no production dependencies. +- The framework offers solutions for different topics, not necessarily an http server. +- The vision of the framework is to offer a code structure to the developer that will help segregate responsibilities in projects through a [SOLID](https://en.wikipedia.org/wiki/SOLID) [OOP](https://en.wikipedia.org/wiki/Object-oriented_programming) approach. + +## Addons + +Consider extending the framework with one or multiple plugins where the extra functionality is required: +*Please note; you should use matching versions for core and the plugins by major and future releases* + +- [core-handlebars](http://github.com/superhero/js.core.handlebars) +- [core-resource](http://github.com/superhero/js.core.resource) +- [core-websocket](http://github.com/superhero/js.core.websocket) + +## Install + +`npm install superhero` + +...or just set the dependency in your `package.json` file: + +```json +{ + "dependencies": + { + "superhero": "*" + } +} +``` + +## Standards + +Recommended standards are published in this repository, stored in the [`doc/standard`](doc/standard) folder. + +***OBS!*** These standards are still being developed, rapid evolution between version 1 and 2 is to be expected. + + - [Microservice (sop-ms)](doc/standard/sop-ms.md) + - [Source code (sop-src)](doc/standard/sop-src.md) + - [API (sop-api)](doc/standard/sop-api.md) + - [Domain (sop-domain)](doc/standard/sop-domain.md) + - [Infrastructure (sop-infrastructure)](doc/standard/sop-infrastructure.md) + - [View (sop-view)](doc/standard/sop-view.md) + - [Test (sop-test)](doc/standard/sop-test.md) + - [Event (sop-event)](doc/standard/sop-event.md) + +## Automatic code and documentation generation + +The package comes with a **CLI** helper (**C**ommand **L**ine **I**nterface). You do not need to use this helper in order to use the framework, but if you do get used to the helper you should be able to speed up your productivity. + +If you install the package globally, by running the folowing command `npm install superhero -g`, then you can use a CLI helper, that can be accessed from the terminal by running the command `superhero`: + +``` + __ + _______ ______ ___ _____/ /_ ___ _________ + / ___/ / / / __ \/ _ \/ ___/ __ \/ _ \/ ___/ __ \ + (__ ) /_/ / /_/ / __/ / / / / / __/ / / /_/ / + /____/\__,_/ .___/\___/_/ /_/ /_/\___/_/ \____/ + /_/ + + +1. Generate project +2. Generate API classes and corresponding tests from configuration +3. Generate API documentation +4. Generate Domain classes from configuration + +Choose your destiny: +``` + +As you can see, there's a menu where you can select different aid for code and documentation generation. + +### Problems installing globally? + +This is a guide on how to install the package globally, if you have problems with permissions, eg: `npm WARN checkPermissions Missing write access to /usr/lib/node_modules` + +#### Solution + +Tested on a debian distribution. Run the following commands: + +``` +mkdir ~/.npm-global +npm config set prefix '~/.npm-global' +``` + +Open or create a `~/.profile` file and add this line: + +``` +export PATH=~/.npm-global/bin:$PATH +``` + +Update your system variables by running: + +``` +source ~/.profile +``` + +Then again, run `npm install superhero -g` + +## Example application + +An example application to get started, covering some recommended "best practices" regarding testing and docs generation. + +The application is a calculator with very basic functionality. The aim with this application is not to make a good calculator. The aim is to show you, who is reading this, how different parts of the framework works, or are intended to work. + +### Example Application › File structure + +``` +super-duper-app +├── docs +├── src +│ ├── api +│ │ ├── endpoint +│ │ │ ├── append-calculation.js +│ │ │ └── create-calculation.js +│ │ ├── middleware +│ │ │ └── authentication.js +│ │ ├── observer +│ │ │ ├── calculation-appended +│ │ │ │ └── log +│ │ │ │ ├── index.js +│ │ │ │ └── locator.js +│ │ │ └── calculation-created +│ │ │ └── log +│ │ │ ├── index.js +│ │ │ └── locator.js +│ │ └── config.js +│ ├── domain +│ │ ├── aggregate +│ │ │ └── calculator +│ │ │ ├── error +│ │ │ │ ├── calculation-could-not-be-found.js +│ │ │ │ └── invalid-calculation-type.js +│ │ │ ├── index.js +│ │ │ └── locator.js +│ │ ├── schema +│ │ │ └── entity +│ │ │ └── calculation.js +│ │ └── config.js +│ └── index.js +├── test +│ ├── integration +│ │ ├── test.calculations.js +│ │ └── test.logger.js +│ ├── init.js +│ └── mocha.opts +├── .gitignore +├── package.json +└── README.md +``` + +The file structure is here divided to 2 different modules + +- `api` *- responsible for the endpoints and observers.* +- `domain` *- responsible for domain logic.* + +#### `.gitignore` + +``` +docs/generated +npm-debug.log +node_modules +package-lock.json +.nyc_output +``` + +Set up a `.gitignore` file to ignore some auto-generated files to keep a clean repository. + +#### `package.json` + +```js +{ + "name": "Super Duper App", + "version": "0.0.1", + "description": "An example application of the superhero framework", + "repository": "https://github.com/...", + "license": "MIT", + "main": "src/index.js", + "author": { + "name": "Padawan", + "email": "padawan@example.com" + }, + "scripts": { + "docs-coverage": "nyc mocha './{,!(node_modules)/**}/test.*.js' --opts ./test/mocha.opts && nyc report --reporter=html --report-dir=./docs/generated/coverage", + "docs-tests": "mocha './{,!(node_modules)/**}/test.*.js' --opts ./test/mocha.opts --reporter mochawesome --reporter-options reportDir=docs/generated/test,reportFilename=index,showHooks=always", + "test": "nyc mocha './{,!(node_modules)/**}/test.*.js' --opts ./test/mocha.opts", + "start": "node ./src/index.js" + }, + "dependencies": { + "superhero": "*" + }, + "devDependencies": { + "mocha": "5.1.0", + "mochawesome": "3.0.2", + "chai": "4.1.2", + "nyc": "11.7.1" + } +} +``` + +Our [`package.json`](https://docs.npmjs.com/files/package.json) file will dictate what dependencies we use. This example application will go through test cases, why we have a few `devDependencies` defined. + +#### `index.js` + +```js +const +CoreFactory = require('superhero/core/factory'), +coreFactory = new CoreFactory, +core = coreFactory.create() + +core.add('api') +core.add('domain') + +core.load() + +core.locate('core/bootstrap').bootstrap().then(() => +core.locate('core/http/server').listen(process.env.HTTP_PORT)) +``` + +We start of by creating a core factory that will create the core object, central to our application. The core is designed to keep track of a global state related to it's context. You can create multiple cores if necessary, but normally it makes sens to only use one. This example will only use one core. + +The next step is to add services that we will use in relation to the core context. Here we add `api` and `domain` that exists in the file tree previously defined. You also notice that we add the service `core/http/server` that does not exist in the file tree we defined. The `core/http/server` service is located in the core, but may not always be necessary to load depending on the scope of your application, so you need to add the `core/http/server` service when you want to set up an http server. +The framework will add services depending on a hierarchy of paths, it's possible to write over the configuration done by one component in another. This feature allows the developer to extend, or build on to, components declared functionality. + +- First it will try to load in relation to your `main script` +- next it attempts by dependency defined in your `package.json` file +- and finally it will attempt to load related to the core library of existing services. + +*This hierarchy allows you, as a developer, to overwrite anything in the framework with custom logic.* + +Next we load the core! This will eager load all the services previously added to the context. + +By bootstrapping the core, we run a few scripts that needs to run for some modules to function properly. As a developer you can hook into this process in the modules you write. + +And in the end, after we have added, loaded and bootstrapped the modules related to the context, we tell the server to listen to a specific port for network activity. In this case, the environment variable `HTTP_PORT``is expected to be set to an integer. + +### Api + +#### `src/api/config.js` + +```js +/** + * @namespace Api + */ +module.exports = +{ + core: + { + http: + { + server: + { + routes: + { + 'create-calculation': + { + url : '/calculations', + method : 'post', + endpoint: 'api/endpoint/create-calculation', + view : 'core/http/server/view/json', + input : false + }, + 'authentication': + { + middleware : + [ + 'api/middleware/authentication' + ] + }, + 'append-calculation': + { + url : '/calculations/:id', + method : 'put', + endpoint: 'api/endpoint/append-calculation', + view : 'core/http/server/view/json', + input : 'entity/calculation' + } + } + } + }, + eventbus: + { + observers: + { + 'calculation created' : { 'api/observer/calculation-created/log' : true }, + 'calculation appended' : { 'api/observer/calculation-appended/log' : true } + } + }, + locator: + { + 'api/observer/calculation-created/log' : __dirname + '/observer/calculation-created/log', + 'api/observer/calculation-appended/log' : __dirname + '/observer/calculation-appended/log' + } + } +} + +``` + +I have here chosen to set up a folder structure with a module called `api`. This module will contain all the endpoints. As such, the config file of this module will specify the router setting for these endpoints. + +I set up two explicit routes: `create-calculation` and `append-calculation`, and one middleware route: `authentication`. + +- The **url** attribute declares what **url pathname** the route will be valid for. +- The **method** attribute declares what **method** the route will be valid for + +The middleware route does not have an action or method constraint specified, so it's considered valid, but it does not have an endpoint specified; declaring it's unterminated. When a route is valid, but unterminated, then it will be merged together with every other valid route specified until one that is terminated appears, eg: one that has declared an endpoint. + +The `api` in this example is also responsible for attaching event listeners to events, or "domain events". In this example, we attach a logger observer to specific events. Notice that the `locator` describes where the observers can be located, then used in the `core.eventbus.observers` config context. + +#### `src/api/endpoint/create-calculation.js` + +```js +const Dispatcher = require('superhero/core/http/server/dispatcher') + +/** + * @memberof Api + * @extends {superhero/core/http/server/dispatcher} + */ +class CreateCalculationEndpoint extends Dispatcher +{ + dispatch() + { + const + calculator = this.locator.locate('domain/aggregate/calculator'), + calculationId = calculator.createCalculation() + + this.view.body.id = calculationId + } +} + +module.exports = CreateCalculationEndpoint +``` + +The `create calculation` endpoint is here defined. + +- First we locate the `calculator` service. +- Next we create a calculation +- And finally we populate the `view model` with the returning calculation id + +***OBS! it's possible to define the `dispatch` method as `async`. The framework will `await` for the method to finish*** + +#### `src/api/endpoint/append-calculation.js` + +```js +const +Dispatcher = require('superhero/core/http/server/dispatcher'), +NotFoundError = require('superhero/core/http/server/dispatcher/error/not-found'), +BadRequestError = require('superhero/core/http/server/dispatcher/error/bad-request') + + +/** + * @memberof Api + * @extends {superhero/core/http/server/dispatcher} + */ +class AppendCalculationEndpoint extends Dispatcher +{ + dispatch() + { + const + calculator = this.locator.locate('domain/aggregate/calculator'), + calculation = this.route.dto, + result = calculator.appendToCalculation(calculation) + + this.view.body.result = result + } + + onError(error) + { + switch(error) + { + case 'E_CALCULATION_COULD_NOT_BE_FOUND': + throw new NotFoundError('Calculation could not be found') + + case 'E_INVALID_CALCULATION_TYPE': + throw new BadRequestError(`Unrecognized type: "${this.route.dto.type}"`) + + default: + throw error + } + } +} + +module.exports = AppendCalculationEndpoint +``` + +The `append calculation` endpoint is here defined. + +- We start by showing you how to access data in the request through the `dto` *(Data Transfer Object)* located on the route. +- Next we locate the `calculator` service. +- Followed by appending a calculation to the stored calculated sum. +- And finally we populate the `view model` with the result of the calculation. + +Apart from the `dispatch` method, this time, we also define an `onError` method. This is the method that will be called if an error is thrown in the `dispatch` method. The first parameter to the `onError` method is the error that was thrown in the `dispatch` method. + +#### `src/api/middleware/authentication.js` + +```js +const +Dispatcher = require('superhero/core/http/server/dispatcher'), +Unauthorized = require('superhero/core/http/server/dispatcher/error/unauthorized') + +/** + * @memberof Api + * @extends {superhero/core/http/server/dispatcher} + */ +class AuthenticationMiddleware extends Dispatcher +{ + async dispatch(next) + { + const + configuration = this.locator.locate('core/configuration'), + apikey = this.request.headers['api-key'] + + if(apikey === 'ABC123456789') + { + await next() + } + else + { + throw new Unauthorized('You are not authorized to access the requested resource') + } + } +} + +module.exports = AuthenticationMiddleware +``` + +This middleware is used for authentication. It's a simple implementation, one should look at using a more robust solution, involving an ACL, when creating a solution for production environment. This example serves a purpose to show a good use-case for when to apply a middleware, not how best practice regarding authentication is defined. + +**OBS!** The post handling (after the `next` callback has been called) will be handled in reversed order. + +``` + Middleware + ↓ ↑ + Middleware + ↓ ↑ + Endpoint +``` + +### Api - Observer + +#### `src/api/observer/calculation-appended/log/index.js` + +```js +/** + * @memberof Api + * @implements {superhero/core/eventbus/observer} + */ +class ObserverCalculationAppendedLog +{ + constructor(console, eventbus) + { + this.console = console + this.eventbus = eventbus + } + + observe(data) + { + this.console.log(data) + this.eventbus.emit('logged calculation appended event', data) + } +} + +module.exports = ObserverCalculationAppendedLog +``` + +*See description below...* + +#### `src/api/observer/calculation-appended/log/locator.js` + +```js +const +ObserverCalculationAppendedLog = require('.'), +LocatorConstituent = require('superhero/core/locator/constituent') + +/** + * @memberof Api + * @extends {superhero/core/locator/constituent} + */ +class ObserverCalculationAppendedLogLocator extends LocatorConstituent +{ + /** + * @returns {ObserverCalculationAppendedLog} + */ + locate() + { + const + console = this.locator.locate('core/console'), + eventbus = this.locator.locate('core/eventbus') + + return new ObserverCalculationAppendedLog(console, eventbus) + } +} + +module.exports = ObserverCalculationAppendedLogLocator +``` + +*See description below...* + +#### `src/api/observer/calculation-created/log/index.js` + +```js +/** + * @memberof Api + * @implements {superhero/core/eventbus/observer} + */ +class ObserverCalculationCreatedLog +{ + constructor(console, eventbus) + { + this.console = console + this.eventbus = eventbus + } + + observe(data) + { + this.console.log(data) + this.eventbus.emit('logged calculation created event', data) + } +} + +module.exports = ObserverCalculationCreatedLog +``` + +*See description below...* + +#### `src/api/observer/calculation-created/log/locator.js` + +```js +const +ObserverCalculationCreatedLog = require('.'), +LocatorConstituent = require('superhero/core/locator/constituent') + +/** + * @memberof Api + * @extends {superhero/core/locator/constituent} + */ +class ObserverCalculationCreatedLogLocator extends LocatorConstituent +{ + /** + * @returns {ObserverCalculationCreatedLog} + */ + locate() + { + const + console = this.locator.locate('core/console'), + eventbus = this.locator.locate('core/eventbus') + + return new ObserverCalculationCreatedLog(console, eventbus) + } +} + +module.exports = ObserverCalculationCreatedLogLocator +``` + +The logger observers simply implements an interface to be recognized as an observer by the `eventbus`. + +After we have logged the event to the console, we emit an event to broadcast that we have logged to the console. When broadcasting an event, we can observe this event in the future, if we like. For instance, when you like to create a test for the method, we can listen to this event to ensure the process is being fulfilled. + +### Domain + +#### `src/domain/aggregate/calculator/index.js` + +```js +const +CalculationCouldNotBeFoundError = require('./error/calculation-could-not-be-found'), +InvalidCalculationTypeError = require('./error/invalid-calculation-type') + +/** + * Calculator service, manages calculations + * @memberof Domain + */ +class AggregateCalculator +{ + /** + * @param {superhero/core/eventbus} eventbus + */ + constructor(eventbus) + { + this.eventbus = eventbus + this.calculations = [] + } + + /** + * @returns {number} the id of the created calculation + */ + createCalculation() + { + const id = this.calculations.push(0) + this.eventbus.emit('calculation created', { id }) + return id + } + + /** + * @throws {E_CALCULATION_COULD_NOT_BE_FOUND} + * @throws {E_INVALID_CALCULATION_TYPE} + * + * @param {CalculatorCalculation} dto + * + * @returns {number} the result of the calculation + */ + appendToCalculation({ id, type, value }) + { + if(id < 1 + || id > this.calculations.length) + { + throw new CalculationCouldNotBeFoundError(`ID out of range: "${id}/${this.calculations.length}"`) + } + + switch(type) + { + case 'addition': + { + const result = this.calculations[id - 1] += value + this.eventbus.emit('calculation appended', { id, type, result }) + return result + } + case 'subtraction': + { + const result = this.calculations[id - 1] -= value + this.eventbus.emit('calculation appended', { id, type, result }) + return result + } + default: + { + throw new InvalidCalculationTypeError(`Unrecognized type used for calculation: "${type}"`) + } + } + } +} + +module.exports = AggregateCalculator +``` + +The calculator aggregate is here defined. Good practice dictate that we should define isolated errors for everything that can go wrong. First we require and define these errors to have access to the type and to be able to trow them when needed. + +It's a simple aggregate that creates a calculation and allows to append an additional calculation to an already created calculation. + +#### `src/domain/aggregate/calculator/locator.js` + +```js +const +Calculator = require('.'), +LocatorConstituent = require('superhero/core/locator/constituent') + +/** + * @memberof Domain + * @extends {superhero/core/locator/constituent} + */ +class AggregateCalculatorLocator extends LocatorConstituent +{ + /** + * @returns {AggregateCalculator} + */ + locate() + { + const eventbus = this.locator.locate('core/eventbus') + return new AggregateCalculator(eventbus) + } +} + +module.exports = AggregateCalculatorLocator +``` + +The locator is responsible for dependency injection related to the aggregates it factor. + +#### `src/domain/aggregate/calculator/error/calculation-could-not-be-found.js` + +```js +/** + * @memberof Domain + * @extends {Error} + */ +class CalculationCouldNotBeFoundError extends Error +{ + constructor(...args) + { + super(...args) + this.code = 'E_CALCULATION_COULD_NOT_BE_FOUND' + } +} + +module.exports = CalculationCouldNotBeFoundError +``` + +A specific error with a specific error code; specifying what specific type of error is thrown. + +#### `src/calculator/error/invalid-calculation-type.js` + +```js +/** + * @memberof Domain + * @extends {Error} + */ +class InvalidCalculationTypeError extends Error +{ + constructor(...args) + { + super(...args) + this.code = 'E_INVALID_CALCULATION_TYPE' + } +} + +module.exports = InvalidCalculationTypeError +``` + +Another specific error... + +#### `src/domain/schema/entity/calculation.js` + +```js +/** + * @memberof Domain + * @typedef {Object} EntityCalculation + * @property {number} id + * @property {string} type + * @property {number} value + */ +const entity = +{ + 'id': + { + 'type' : 'schema', + 'schema' : 'value-object/id', + 'trait' : 'id' + }, + 'type': + { + 'type': 'string', + 'enum': + [ + 'addition', + 'subtraction' + ] + }, + 'value': + { + 'type': 'decimal' + } +} + +module.exports = entity +``` + +#### `src/domain/schema/value-object/id.js` + +```js +/** + * @memberof Domain + * @typedef {Object} ValueObjectId + * @property {number} id + */ +const valueObject = +{ + 'id': + { + 'type' : 'integer', + 'unsigned': true + } +} + +module.exports = valueObject +``` + +Defining a JSON schema for an entity; calculation. It's a good praxis to also define the type in "jsdoc", as seen above. + +A table over validation and filtration rules follows... + +| | csv | boolean | decimal | integer | json | schema | string | timestamp | +|----------------------|-----------|-----------|-----------|-----------|-----------|-----------|-----------|-----------| +| default | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | +| collection | boolean | boolean | boolean | boolean | boolean | boolean | boolean | boolean | +| collection-size-min | number | number | number | number | number | number | number | number | +| collection-size-max | number | number | number | number | number | number | number | number | +| optional | boolean | boolean | boolean | boolean | boolean | boolean | boolean | boolean | +| nullable | boolean | boolean | boolean | boolean | boolean | boolean | boolean | boolean | +| unsigned | ✗ | ✗ | boolean | boolean | ✗ | ✗ | ✗ | ✗ | +| min | number | ✗ | number | number | ✗ | ✗ | number | timestamp | +| max | number | ✗ | number | number | ✗ | ✗ | number | timestamp | +| gt | number | ✗ | number | number | ✗ | ✗ | number | timestamp | +| lt | number | ✗ | number | number | ✗ | ✗ | number | timestamp | +| length | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | number | ✗ | +| enum | array | array | array | array | ✗ | ✗ | array | array | +| uppercase | boolean | ✗ | ✗ | ✗ | ✗ | ✗ | boolean | ✗ | +| lowercase | boolean | ✗ | ✗ | ✗ | ✗ | ✗ | boolean | ✗ | +| not-empty | boolean | ✗ | ✗ | ✗ | ✗ | ✗ | boolean | ✗ | +| stringified | ✗ | ✗ | ✗ | ✗ | boolean | ✗ | ✗ | ✗ | +| indentation | ✗ | ✗ | ✗ | ✗ | number | ✗ | ✗ | ✗ | +| schema | ✗ | ✗ | ✗ | ✗ | ✗ | string | ✗ | ✗ | +| trait | ✗ | ✗ | ✗ | ✗ | ✗ | string | ✗ | ✗ | + +#### `src/domain/config.js` + +```js +/** + * @namespace Domain + */ +module.exports = +{ + core: + { + schema: + { + composer: + { + 'entity/calculation' : __dirname + '/schema/entity/calculation' + } + }, + locator: + { + 'domain/aggregate/calculator' : __dirname + '/aggregate/calculator' + } + } +} +``` + +In the `domain` `config` we define where a path to a module is located, and where the `locator` can find a constituent `locator` to locate/find a service through. + +### Test + +#### `test/mocha.opts` + +``` +--require test/init.js +--ui bdd +--full-trace +--timeout 5000 +``` + +There will probably be a lot of [settings you need to set for mocha](https://mochajs.org/api/mocha), sooner or later; just as well that we make it a praxis to define the options outside your `package.json` file. + +#### `test/init.js` + +```js +require.main.filename = __dirname + '/../src/index.js' +require.main.dirname = __dirname + '/../src' +``` + +The init script must set some variables for the core to function as expected in testing. + +The port is by design set through the environment variable `HTTP_PORT`. While testing we can set the variable to what ever we like, and what ever is suitable for the local machine we are on. + +#### `test/test.calculations.js` + +```js +describe('Calculations', () => +{ + const + expect = require('chai').expect, + context = require('mochawesome/addContext') + + let core + + before((done) => + { + const + CoreFactory = require('superhero/core/factory'), + coreFactory = new CoreFactory + + core = coreFactory.create() + + core.add('domain') + core.add('api') + + core.load() + + core.locate('core/bootstrap').bootstrap().then(() => + { + core.locate('core/http/server').listen(9001) + core.locate('core/http/server').onListening(done) + }) + }) + + after(() => + { + core.locate('core/http/server').close() + }) + + it('A client can create a calculation', async function() + { + const configuration = core.locate('core/configuration') + const httpRequest = core.locate('core/http/request') + context(this, { title:'route', value:configuration.find('core.http.server.routes.create-calculation') }) + const response = await httpRequest.post('http://localhost:9001/calculations') + expect(response.data.id).to.be.equal(1) + }) + + it('A client can append a calculation to the result of a former calculation if authentication Api-Key', async function() + { + const configuration = core.locate('core/configuration') + const httpRequest = core.locate('core/http/request') + context(this, { title:'route', value:configuration.find('core.http.server.routes.append-calculation') }) + const url = 'http://localhost:9001/calculations/1' + const data = { id:1, type:'addition', value:100 } + const response_unauthorized = await httpRequest.put({ url, data }) + expect(response_unauthorized.status).to.be.equal(401) + const headers = { 'Api-Key':'ABC123456789' } + const response_authorized = await httpRequest.put({ headers, url, data }) + expect(response_authorized.data.result).to.be.equal(data.value) + }) +}) +``` + +#### `test/test.logger.js` + +```js +describe('Logger', () => +{ + const + expect = require('chai').expect, + context = require('mochawesome/addContext') + + let core + + before((done) => + { + const + CoreFactory = require('superhero/core/factory'), + coreFactory = new CoreFactory + + core = coreFactory.create() + + core.add('domain') + core.add('api') + + core.load() + + core.locate('core/bootstrap').bootstrap().then(done) + }) + + it('the logger is logging', function(done) + { + const configuration = core.locate('core/configuration') + const eventbus = core.locate('core/eventbus') + context(this, { title:'observers', value:configuration.find('core.http.eventbus.observers') }) + eventbus.once('logged calculation created event', () => done()) + eventbus.emit('calculation created', 'test') + }) +}) +``` + +Finally I have designed a few simple tests that proves the pattern, and gives insight to the expected interface. Here I suggest using a [BDD](https://en.wikipedia.org/wiki/Behavior-driven_development) approach, though my examples are not aligned with the principles. + +### Npm scripts + +#### To run the application: + +``` +npm install --production +npm start +``` + +#### To test the application: + +``` +npm install +npm test +``` + +#### For an auto-generated coverage report in html: + +``` +npm run docs-coverage +``` + +#### For an auto-generated test report in html: + +``` +npm run docs-tests +``` diff --git a/bin/superhero.js b/bin/superhero.js index 2b59e08..725a547 100644 --- a/bin/superhero.js +++ b/bin/superhero.js @@ -1,42 +1,42 @@ -#!/usr/bin/env node - -const -readline = require('readline'), -Cli = require('../core/cli'), -cli = new Cli(readline), -generateProject = require('./superhero/generate-project'), -generateApi = require('./superhero/generate-api'), -generateApiDocs = require('./superhero/generate-api-docs'), -generateDomain = require('./superhero/generate-domain'), -run = async () => -{ - // starting - cli.write(` - __ - _______ ______ ___ _____/ /_ ___ _________ - / ___/ / / / __ \\/ _ \\/ ___/ __ \\/ _ \\/ ___/ __ \\ - (__ ) /_/ / /_/ / __/ / / / / / __/ / / /_/ / - /____/\\__,_/ .___/\\___/_/ /_/ /_/\\___/_/ \\____/ - /_/ -`, 'blue') - - cli.write(` -1. Generate project -2. Generate API classes and corresponding tests from configuration -3. Generate API documentation -4. Generate Domain classes from configuration -`) - - const option = await cli.question(`Choose your destiny:`, ['1', '2', '3', '4']) - cli.write(' ✔ Excellent\n', 'green') - - switch(option) - { - case '1': await generateProject(cli); break; - case '2': await generateApi(cli); break; - case '3': await generateApiDocs(cli); break; - case '4': await generateDomain(cli); break; - } -} - -run() +#!/usr/bin/env node + +const +readline = require('readline'), +Cli = require('../core/cli'), +cli = new Cli(readline), +generateProject = require('./superhero/generate-project'), +generateApi = require('./superhero/generate-api'), +generateApiDocs = require('./superhero/generate-api-docs'), +generateDomain = require('./superhero/generate-domain'), +run = async () => +{ + // starting + cli.write(` + __ + _______ ______ ___ _____/ /_ ___ _________ + / ___/ / / / __ \\/ _ \\/ ___/ __ \\/ _ \\/ ___/ __ \\ + (__ ) /_/ / /_/ / __/ / / / / / __/ / / /_/ / + /____/\\__,_/ .___/\\___/_/ /_/ /_/\\___/_/ \\____/ + /_/ +`, 'blue') + + cli.write(` +1. Generate project +2. Generate API classes and corresponding tests from configuration +3. Generate API documentation +4. Generate Domain classes from configuration +`) + + const option = await cli.question(`Choose your destiny:`, ['1', '2', '3', '4']) + cli.write(' ✔ Excellent\n', 'green') + + switch(option) + { + case '1': await generateProject(cli); break; + case '2': await generateApi(cli); break; + case '3': await generateApiDocs(cli); break; + case '4': await generateDomain(cli); break; + } +} + +run() diff --git a/bin/superhero/file-template/api-docs.js b/bin/superhero/file-template/api-docs.js index b0000bb..f489a9c 100644 --- a/bin/superhero/file-template/api-docs.js +++ b/bin/superhero/file-template/api-docs.js @@ -1,567 +1,567 @@ -module.exports = (wd, api_config, schemas) => -{ - const MIN_DEPTH = 1 - - function buildParameters(schemaName) - { - if(schemaName) - { - let htmlOutput = `

Parameters

-
-
-
- Name -
-
- Description -
-
` - - const - schemaPath = schemas[schemaName], - parameters = buildParametersFromSchema(schemaPath) - - for(const parameter of parameters) - { - htmlOutput += `
-
-
- ${parameter.name} -
-
- ${parameter.type} ${parameter.schema ? `(${parameter.schema})` : ''} -
-
-
- ${parameter.description ? parameter.description : ''} - ${buildValidationRulesOutput(parameter.validationRules)} -
-
` - } - - htmlOutput += '
' - - return htmlOutput - } - - return '' - } - - function buildJSONExample(schemaName, stringify = true, depth = 0) - { - if(schemaName) - { - const - schemaPath = schemas[schemaName], - parameters = buildParametersFromSchema(schemaPath) - - if(parameters.length > 0) - { - let example = {} - for(const parameter of parameters) - { - const - isSameSchema = parameter.schema === schemaName, - isCollection = parameter.collection - - if(parameter.example !== undefined) - { - example[parameter.name] = parameter.example - } - else if(parameter.schema) - { - if(!isSameSchema) - { - if(isCollection) - { - example[parameter.name] = [buildJSONExample(parameter.schema, false)] - } - else - { - example[parameter.name] = buildJSONExample(parameter.schema, false) - } - } - else if(isSameSchema) - { - if(depth <= MIN_DEPTH) - { - const nextLevel = depth + 1 - if(isCollection) - { - example[parameter.name] = [buildJSONExample(parameter.schema, false, nextLevel)] - } - else - { - example[parameter.name] = buildJSONExample(parameter.schema, false, nextLevel) - } - } - else - { - if(isCollection) - { - if(parameter.default && Array.isArray(parameter.default)) - example[parameter.name] = parameter.default - else if(parameter.default) - example[parameter.name] = [parameter.default] - else - example[parameter.name] = [] - } - else - { - example[parameter.name] = parameter.default ? parameter.default : undefined - } - } - } - } - } - - return stringify ? JSON.stringify(example, null, 2).replace(/\n/gi, ' ').replace(/\\"/gi, '"') : example - } - } - - return '// none' - } - - function buildOutputExample(output) - { - let htmlOutput = `

Output

-
- -
` - - return htmlOutput - } - - function buildInputExample(input) - { - let htmlOutput = `

Input

-
- -
` - - return htmlOutput - } - - function buildValidationRulesOutput(validationRules) - { - if(Object.keys(validationRules).length !== 0) - { - let output = `

Validation rules

- ' - - return output - } - - return '' - } - - function getValidationRules(parameter) - { - const validationRules = { ...parameter } - - delete validationRules.description - delete validationRules.optional - delete validationRules.type - delete validationRules.example - delete validationRules.schema - delete validationRules.trait - - return validationRules - } - - function buildParametersFromSchema(schemaPath) - { - let parameters = [] - - if(schemaPath) - { - const schema = require(schemaPath) - - for(const attribute in schema) - { - if(attribute === '@meta' && schema[attribute].extends) - { - if(Array.isArray(schema[attribute].extends)) - { - for(const s of schema[attribute].extends) - { - const - schemaPath = schemas[s], - schemaParameters = buildParametersFromSchema(schemaPath) - - parameters = parameters.concat(schemaParameters) - } - } - else if(typeof schema[attribute].extends === 'string') - { - const - schemaPath = schemas[schema[attribute].extends], - schemaParameters = buildParametersFromSchema(schemaPath) - - parameters = parameters.concat(schemaParameters) - } - } - else if(schema[attribute].type === 'schema' && schema[attribute].hasOwnProperty('trait')) - { - const - traitSchema = require(schemas[schema[attribute].schema]), - trait = schema[attribute].trait - - parameters.push({ - name: attribute, - description: schema[attribute].description? schema[attribute].description : traitSchema[trait].description, - required: schema[attribute].optional ? false : true, - type: traitSchema[trait].type, - example: traitSchema[trait].example, - validationRules: getValidationRules(traitSchema[trait]) - }) - } - else if(schema[attribute].type === 'schema') - { - parameters.push({ - name: attribute, - description: schema[attribute].description, - required: schema[attribute].optional ? false : true, - type: schema[attribute].type, - schema: schema[attribute].schema, - collection : schema[attribute].collection, - validationRules: getValidationRules(schema[attribute]) - }) - } - else if(typeof schema[attribute].extends === 'string') - { - parameters.push({ - name: attribute, - description: schema[attribute].description, - required: schema[attribute].optional ? false : true, - type: schema[attribute].type, - example: schema[attribute].example, - validationRules: getValidationRules(schema[attribute]) - }) - } - else - { - parameters.push({ - name: attribute, - description: schema[attribute].description, - required: schema[attribute].optional ? false : true, - type: schema[attribute].type, - example: schema[attribute].example, - validationRules: getValidationRules(schema[attribute]) - }) - } - } - } - - return parameters - } - - let html = ` - - - - - - API Documentation - - - - - - - - - ` - - let baseView - html += '

Endpoints

' - for(const route in api_config) - { - if(api_config[route].hasOwnProperty('url')) - { - html += ` -
- ${api_config[route].description ? `

${api_config[route].description}

` : ''} -

View

-

${api_config[route].view ? api_config[route].view : baseView}

- ${buildParameters(api_config[route].input)} - ${buildInputExample(api_config[route].input)} - ${buildOutputExample(api_config[route].output)} -
` - } - else if(api_config[route].hasOwnProperty('view')) - { - baseView = api_config[route].view - } - } - - html += '

Schemas

' - for(const schema in schemas) - { - html += ` -
- ${buildParameters(schema)} - ${buildInputExample(schema)} -
` - } - - html += ` - - - - -` - return html -} +module.exports = (wd, api_config, schemas) => +{ + const MIN_DEPTH = 1 + + function buildParameters(schemaName) + { + if(schemaName) + { + let htmlOutput = `

Parameters

+
+
+
+ Name +
+
+ Description +
+
` + + const + schemaPath = schemas[schemaName], + parameters = buildParametersFromSchema(schemaPath) + + for(const parameter of parameters) + { + htmlOutput += `
+
+
+ ${parameter.name} +
+
+ ${parameter.type} ${parameter.schema ? `(${parameter.schema})` : ''} +
+
+
+ ${parameter.description ? parameter.description : ''} + ${buildValidationRulesOutput(parameter.validationRules)} +
+
` + } + + htmlOutput += '
' + + return htmlOutput + } + + return '' + } + + function buildJSONExample(schemaName, stringify = true, depth = 0) + { + if(schemaName) + { + const + schemaPath = schemas[schemaName], + parameters = buildParametersFromSchema(schemaPath) + + if(parameters.length > 0) + { + let example = {} + for(const parameter of parameters) + { + const + isSameSchema = parameter.schema === schemaName, + isCollection = parameter.collection + + if(parameter.example !== undefined) + { + example[parameter.name] = parameter.example + } + else if(parameter.schema) + { + if(!isSameSchema) + { + if(isCollection) + { + example[parameter.name] = [buildJSONExample(parameter.schema, false)] + } + else + { + example[parameter.name] = buildJSONExample(parameter.schema, false) + } + } + else if(isSameSchema) + { + if(depth <= MIN_DEPTH) + { + const nextLevel = depth + 1 + if(isCollection) + { + example[parameter.name] = [buildJSONExample(parameter.schema, false, nextLevel)] + } + else + { + example[parameter.name] = buildJSONExample(parameter.schema, false, nextLevel) + } + } + else + { + if(isCollection) + { + if(parameter.default && Array.isArray(parameter.default)) + example[parameter.name] = parameter.default + else if(parameter.default) + example[parameter.name] = [parameter.default] + else + example[parameter.name] = [] + } + else + { + example[parameter.name] = parameter.default ? parameter.default : undefined + } + } + } + } + } + + return stringify ? JSON.stringify(example, null, 2).replace(/\n/gi, ' ').replace(/\\"/gi, '"') : example + } + } + + return '// none' + } + + function buildOutputExample(output) + { + let htmlOutput = `

Output

+
+ +
` + + return htmlOutput + } + + function buildInputExample(input) + { + let htmlOutput = `

Input

+
+ +
` + + return htmlOutput + } + + function buildValidationRulesOutput(validationRules) + { + if(Object.keys(validationRules).length !== 0) + { + let output = `

Validation rules

+ ' + + return output + } + + return '' + } + + function getValidationRules(parameter) + { + const validationRules = { ...parameter } + + delete validationRules.description + delete validationRules.optional + delete validationRules.type + delete validationRules.example + delete validationRules.schema + delete validationRules.trait + + return validationRules + } + + function buildParametersFromSchema(schemaPath) + { + let parameters = [] + + if(schemaPath) + { + const schema = require(schemaPath) + + for(const attribute in schema) + { + if(attribute === '@meta' && schema[attribute].extends) + { + if(Array.isArray(schema[attribute].extends)) + { + for(const s of schema[attribute].extends) + { + const + schemaPath = schemas[s], + schemaParameters = buildParametersFromSchema(schemaPath) + + parameters = parameters.concat(schemaParameters) + } + } + else if(typeof schema[attribute].extends === 'string') + { + const + schemaPath = schemas[schema[attribute].extends], + schemaParameters = buildParametersFromSchema(schemaPath) + + parameters = parameters.concat(schemaParameters) + } + } + else if(schema[attribute].type === 'schema' && schema[attribute].hasOwnProperty('trait')) + { + const + traitSchema = require(schemas[schema[attribute].schema]), + trait = schema[attribute].trait + + parameters.push({ + name: attribute, + description: schema[attribute].description? schema[attribute].description : traitSchema[trait].description, + required: schema[attribute].optional ? false : true, + type: traitSchema[trait].type, + example: traitSchema[trait].example, + validationRules: getValidationRules(traitSchema[trait]) + }) + } + else if(schema[attribute].type === 'schema') + { + parameters.push({ + name: attribute, + description: schema[attribute].description, + required: schema[attribute].optional ? false : true, + type: schema[attribute].type, + schema: schema[attribute].schema, + collection : schema[attribute].collection, + validationRules: getValidationRules(schema[attribute]) + }) + } + else if(typeof schema[attribute].extends === 'string') + { + parameters.push({ + name: attribute, + description: schema[attribute].description, + required: schema[attribute].optional ? false : true, + type: schema[attribute].type, + example: schema[attribute].example, + validationRules: getValidationRules(schema[attribute]) + }) + } + else + { + parameters.push({ + name: attribute, + description: schema[attribute].description, + required: schema[attribute].optional ? false : true, + type: schema[attribute].type, + example: schema[attribute].example, + validationRules: getValidationRules(schema[attribute]) + }) + } + } + } + + return parameters + } + + let html = ` + + + + + + API Documentation + + + + + + + + + ` + + let baseView + html += '

Endpoints

' + for(const route in api_config) + { + if(api_config[route].hasOwnProperty('url')) + { + html += ` +
+ ${api_config[route].description ? `

${api_config[route].description}

` : ''} +

View

+

${api_config[route].view ? api_config[route].view : baseView}

+ ${buildParameters(api_config[route].input)} + ${buildInputExample(api_config[route].input)} + ${buildOutputExample(api_config[route].output)} +
` + } + else if(api_config[route].hasOwnProperty('view')) + { + baseView = api_config[route].view + } + } + + html += '

Schemas

' + for(const schema in schemas) + { + html += ` +
+ ${buildParameters(schema)} + ${buildInputExample(schema)} +
` + } + + html += ` + + + + +` + return html +} diff --git a/bin/superhero/file-template/config-api.js b/bin/superhero/file-template/config-api.js index 2d25d0d..037b80f 100644 --- a/bin/superhero/file-template/config-api.js +++ b/bin/superhero/file-template/config-api.js @@ -1,43 +1,43 @@ -module.exports = (ns) => -`/** - * @namespace ${ns} - */ -module.exports = -{ - core: - { - http: - { - server: - { - routes: - { - 'json-view': - { - view : 'core/http/server/view/json' - }, - 'example': - { - url : '/v1/example', - method : 'get', - endpoint : 'api/endpoint/example', - input : 'event/requested-example', - output : 'event/-eventlog-result' - } - } - } - }, - eventbus: - { - observers: - { - 'example' : { 'observer/example' : true } - } - }, - locator: - { - 'observer/*' : __dirname + '/observer/*' - } - } -} -` +module.exports = (ns) => +`/** + * @namespace ${ns} + */ +module.exports = +{ + core: + { + http: + { + server: + { + routes: + { + 'json-view': + { + view : 'core/http/server/view/json' + }, + 'example': + { + url : '/v1/example', + method : 'get', + endpoint : 'api/endpoint/example', + input : 'event/requested-example', + output : 'event/-eventlog-result' + } + } + } + }, + eventbus: + { + observers: + { + 'example' : { 'observer/example' : true } + } + }, + locator: + { + 'observer/*' : __dirname + '/observer/*' + } + } +} +` diff --git a/bin/superhero/file-template/config-domain.js b/bin/superhero/file-template/config-domain.js index a5fc8cb..e13392b 100644 --- a/bin/superhero/file-template/config-domain.js +++ b/bin/superhero/file-template/config-domain.js @@ -1,25 +1,25 @@ -module.exports = (ns) => -`/** - * @namespace ${ns} - */ -module.exports = -{ - core: - { - schema: - { - composer: - { - 'event/*' : __dirname + '/schema/event/*', - 'value-object/*' : __dirname + '/schema/value-object/*', - 'entity/*' : __dirname + '/schema/entity/*' - } - }, - locator: - { - 'aggregate/*' : __dirname + '/aggregate/*', - 'service/*' : __dirname + '/service/*' - } - } -} -` +module.exports = (ns) => +`/** + * @namespace ${ns} + */ +module.exports = +{ + core: + { + schema: + { + composer: + { + 'event/*' : __dirname + '/schema/event/*', + 'value-object/*' : __dirname + '/schema/value-object/*', + 'entity/*' : __dirname + '/schema/entity/*' + } + }, + locator: + { + 'aggregate/*' : __dirname + '/aggregate/*', + 'service/*' : __dirname + '/service/*' + } + } +} +` diff --git a/bin/superhero/file-template/config-infrastructure.js b/bin/superhero/file-template/config-infrastructure.js index 8546e63..c919f0d 100644 --- a/bin/superhero/file-template/config-infrastructure.js +++ b/bin/superhero/file-template/config-infrastructure.js @@ -1,15 +1,15 @@ -module.exports = (ns) => -`/** - * @namespace ${ns} - */ -module.exports = -{ - core: - { - locator: - { - 'repository/*' : __dirname + '/*' - } - } -} -` +module.exports = (ns) => +`/** + * @namespace ${ns} + */ +module.exports = +{ + core: + { + locator: + { + 'repository/*' : __dirname + '/*' + } + } +} +` diff --git a/bin/superhero/file-template/config.js b/bin/superhero/file-template/config.js index b93ab7d..73375a1 100644 --- a/bin/superhero/file-template/config.js +++ b/bin/superhero/file-template/config.js @@ -1,9 +1,9 @@ -module.exports = (ns) => -`/** - * @namespace ${ns} - */ -module.exports = -{ - -} -` +module.exports = (ns) => +`/** + * @namespace ${ns} + */ +module.exports = +{ + +} +` diff --git a/bin/superhero/file-template/dispatcher.js b/bin/superhero/file-template/dispatcher.js index 8c650d2..d921949 100644 --- a/bin/superhero/file-template/dispatcher.js +++ b/bin/superhero/file-template/dispatcher.js @@ -1,25 +1,25 @@ -module.exports = (endpoint_name) => -`const Dispatcher = require('superhero/core/http/server/dispatcher') - -/** -* @memberof Api -* @extends {superhero/core/http/server/dispatcher} -*/ -class Endpoint${endpoint_name} extends Dispatcher -{ - async dispatch() - { - - } - - onError(error) - { - switch(error.code) - { - default: throw error - } - } -} - -module.exports = Endpoint${endpoint_name} -` +module.exports = (endpoint_name) => +`const Dispatcher = require('superhero/core/http/server/dispatcher') + +/** +* @memberof Api +* @extends {superhero/core/http/server/dispatcher} +*/ +class Endpoint${endpoint_name} extends Dispatcher +{ + async dispatch() + { + + } + + onError(error) + { + switch(error.code) + { + default: throw error + } + } +} + +module.exports = Endpoint${endpoint_name} +` diff --git a/bin/superhero/file-template/dockerfile.js b/bin/superhero/file-template/dockerfile.js index e4cc41e..8a51a85 100644 --- a/bin/superhero/file-template/dockerfile.js +++ b/bin/superhero/file-template/dockerfile.js @@ -1,18 +1,18 @@ -module.exports = (timezone) => -`FROM node:12.4-stretch-slim - -ENV DEBIAN_FRONTEND noninteractive -ENV HTTP_PORT 80 - -COPY package.json /opt/superhero-application/package.json -COPY src /opt/superhero-application/src -COPY test /opt/superhero-application/test - -WORKDIR /opt/superhero-application - -RUN ln -snf /usr/share/zoneinfo/${timezone} /etc/localtime && echo "${timezone}" > /etc/timezone - -RUN npm install --production - -CMD [ "npm", "start" ] -` +module.exports = (timezone) => +`FROM node:12.4-stretch-slim + +ENV DEBIAN_FRONTEND noninteractive +ENV HTTP_PORT 80 + +COPY package.json /opt/superhero-application/package.json +COPY src /opt/superhero-application/src +COPY test /opt/superhero-application/test + +WORKDIR /opt/superhero-application + +RUN ln -snf /usr/share/zoneinfo/${timezone} /etc/localtime && echo "${timezone}" > /etc/timezone + +RUN npm install --production + +CMD [ "npm", "start" ] +` diff --git a/bin/superhero/file-template/gitignore.js b/bin/superhero/file-template/gitignore.js index 23def27..a90de0e 100644 --- a/bin/superhero/file-template/gitignore.js +++ b/bin/superhero/file-template/gitignore.js @@ -1,7 +1,7 @@ -module.exports = () => -`docs/generated -npm-debug.log -node_modules -package-lock.json -.nyc_output -` +module.exports = () => +`docs/generated +npm-debug.log +node_modules +package-lock.json +.nyc_output +` diff --git a/bin/superhero/file-template/main.js b/bin/superhero/file-template/main.js index bc97a95..df40389 100644 --- a/bin/superhero/file-template/main.js +++ b/bin/superhero/file-template/main.js @@ -1,16 +1,16 @@ -module.exports = (infrastructure, view) => -`const -CoreFactory = require('superhero/core/factory'), -coreFactory = new CoreFactory, -core = coreFactory.create() - -core.add('api') -core.add('domain') -${infrastructure === "no" ? "" : "core.add('infrastructure')"} -${view === "no" ? "" : "core.add('view')"} - -core.load() - -core.locate('core/bootstrap').bootstrap().then(() => -core.locate('core/http/server').listen(process.env.HTTP_PORT)) -` +module.exports = (infrastructure, view) => +`const +CoreFactory = require('superhero/core/factory'), +coreFactory = new CoreFactory, +core = coreFactory.create() + +core.add('api') +core.add('domain') +${infrastructure === "no" ? "" : "core.add('infrastructure')"} +${view === "no" ? "" : "core.add('view')"} + +core.load() + +core.locate('core/bootstrap').bootstrap().then(() => +core.locate('core/http/server').listen(process.env.HTTP_PORT)) +` diff --git a/bin/superhero/file-template/package-json.js b/bin/superhero/file-template/package-json.js index cc5b993..64f732e 100644 --- a/bin/superhero/file-template/package-json.js +++ b/bin/superhero/file-template/package-json.js @@ -1,39 +1,39 @@ -module.exports = (name, description, repository, version) => -`{ - "name": "${name}", - "version": "0.0.1", - "description": "${description.replace('"', '\\"')}", - "repository": "${repository}", - "license": "UNLICENSED", - "main": "src/index.js", - "scripts": { - "docs-coverage": "nyc mocha && nyc report --reporter=html --report-dir=./docs/generated/coverage", - "docs-tests": "mocha --reporter mochawesome --reporter-options reportDir=docs/generated/test,reportFilename=index,showHooks=always", - "test": "nyc mocha", - "start": "node ./src/index.js" - }, - "dependencies": { - "superhero": "${version}" - }, - "devDependencies": { - "mocha": "7.1.2", - "mochawesome": "6.1.1", - "mocha-steps": "1.3.0", - "chai": "4.2.0", - "nyc": "14.1.1" - }, - "mocha": { - "require": [ - "test/init.js", - "mocha-steps" - ], - "ui": "bdd", - "full-trace": true, - "timeout": 5000, - "spec": [ - "./test/**/*.test.js", - "./src/**/*.test.js" - ] - } -} -` +module.exports = (name, description, repository, version) => +`{ + "name": "${name}", + "version": "0.0.1", + "description": "${description.replace('"', '\\"')}", + "repository": "${repository}", + "license": "UNLICENSED", + "main": "src/index.js", + "scripts": { + "docs-coverage": "nyc mocha && nyc report --reporter=html --report-dir=./docs/generated/coverage", + "docs-tests": "mocha --reporter mochawesome --reporter-options reportDir=docs/generated/test,reportFilename=index,showHooks=always", + "test": "nyc mocha", + "start": "node ./src/index.js" + }, + "dependencies": { + "superhero": "${version}" + }, + "devDependencies": { + "mocha": "7.1.2", + "mochawesome": "6.1.1", + "mocha-steps": "1.3.0", + "chai": "4.2.0", + "nyc": "14.1.1" + }, + "mocha": { + "require": [ + "test/init.js", + "mocha-steps" + ], + "ui": "bdd", + "full-trace": true, + "timeout": 5000, + "spec": [ + "./test/**/*.test.js", + "./src/**/*.test.js" + ] + } +} +` diff --git a/bin/superhero/file-template/readme-md.js b/bin/superhero/file-template/readme-md.js index 0db1b68..c3d3194 100644 --- a/bin/superhero/file-template/readme-md.js +++ b/bin/superhero/file-template/readme-md.js @@ -1,2 +1,2 @@ -module.exports = (name) => `# ${name} -` +module.exports = (name) => `# ${name} +` diff --git a/bin/superhero/file-template/schema.js b/bin/superhero/file-template/schema.js index a85ecb6..71d52a7 100644 --- a/bin/superhero/file-template/schema.js +++ b/bin/superhero/file-template/schema.js @@ -1,10 +1,10 @@ -module.exports = (schema_name) => -`/** - * @memberof Domain - * @typedef {Object} Schema${schema_name} - */ -module.exports = -{ - -} -` +module.exports = (schema_name) => +`/** + * @memberof Domain + * @typedef {Object} Schema${schema_name} + */ +module.exports = +{ + +} +` diff --git a/bin/superhero/file-template/test-endpoint.js b/bin/superhero/file-template/test-endpoint.js index b952a20..be6bccf 100644 --- a/bin/superhero/file-template/test-endpoint.js +++ b/bin/superhero/file-template/test-endpoint.js @@ -1,58 +1,58 @@ -module.exports = (endpoint) => -`describe('Endpoint tests for ${endpoint}', () => -{ - const - expect = require('chai').expect, - context = require('mochawesome/addContext'), - port = 9001, - host = 'http://localhost:' + port - - let core - - before((done) => - { - const - CoreFactory = require('superhero/core/factory'), - coreFactory = new CoreFactory - - core = coreFactory.create() - - core.add('api') - core.add('domain') - core.add('infrastructure') - - core.load() - - core.locate('core/bootstrap').bootstrap().then(() => - { - core.locate('core/http/server').listen(port) - core.locate('core/http/server').onListening(done) - }) - }) - - after(async () => - { - core.locate('core/http/server').close() - }) - - it('is expected for the response to match expected output schema', async function() - { - const - configuration = core.locate('core/configuration'), - request = core.locate('core/http/request'), - composer = core.locate('core/schema/composer'), - route = configuration.find('core.http.server.routes.${endpoint}') - - context(this, { title:'route', value:route }) - - const - headers = { 'content-type':'application/json' }, - url = \`\${host}\${route.url}\`, - data = composer.composeExample(route.input), - response = await request[route.method]({ headers, url, data }), - validate = composer.compose.bind(composer, route.output, response.data) - - expect(validate).to.not.throw() - }) -}) -` +module.exports = (endpoint) => +`describe('Endpoint tests for ${endpoint}', () => +{ + const + expect = require('chai').expect, + context = require('mochawesome/addContext'), + port = 9001, + host = 'http://localhost:' + port + + let core + + before((done) => + { + const + CoreFactory = require('superhero/core/factory'), + coreFactory = new CoreFactory + + core = coreFactory.create() + + core.add('api') + core.add('domain') + core.add('infrastructure') + + core.load() + + core.locate('core/bootstrap').bootstrap().then(() => + { + core.locate('core/http/server').listen(port) + core.locate('core/http/server').onListening(done) + }) + }) + + after(async () => + { + core.locate('core/http/server').close() + }) + + it('is expected for the response to match expected output schema', async function() + { + const + configuration = core.locate('core/configuration'), + request = core.locate('core/http/request'), + composer = core.locate('core/schema/composer'), + route = configuration.find('core.http.server.routes.${endpoint}') + + context(this, { title:'route', value:route }) + + const + headers = { 'content-type':'application/json' }, + url = \`\${host}\${route.url}\`, + data = composer.composeExample(route.input), + response = await request[route.method]({ headers, url, data }), + validate = composer.compose.bind(composer, route.output, response.data) + + expect(validate).to.not.throw() + }) +}) +` diff --git a/bin/superhero/file-template/test-init.js b/bin/superhero/file-template/test-init.js index c1d49db..36c6de2 100644 --- a/bin/superhero/file-template/test-init.js +++ b/bin/superhero/file-template/test-init.js @@ -1,4 +1,4 @@ -module.exports = () => -`require.main.filename = __dirname + '/../src/index.js' -require.main.dirname = __dirname + '/../src' -` +module.exports = () => +`require.main.filename = __dirname + '/../src/index.js' +require.main.dirname = __dirname + '/../src' +` diff --git a/bin/superhero/fs/index.js b/bin/superhero/fs/index.js index 2d46807..8c53074 100644 --- a/bin/superhero/fs/index.js +++ b/bin/superhero/fs/index.js @@ -1,40 +1,40 @@ -const fs = require('fs') - -class Fs -{ - /** - * @param {string} wd Working directory - * @param {string} cli Command line interface - */ - constructor(wd, cli) - { - this.wd = wd - this.cli = cli - } - - mkdir(dir) - { - this.cli.write(' Creating path: ' + dir, 'green') - const path = this.wd + '/' + dir - fs.mkdirSync(path, { recursive: true }) - } - - writeFile(file, content) - { - const - path = this.wd + '/' + file, - file_exists = fs.existsSync(path) - - if(file_exists) - { - this.cli.write(' Existing file: ' + file, 'red') - } - else - { - this.cli.write(' Creating file: ' + file, 'green') - fs.writeFileSync(path, content) - } - } -} - -module.exports = Fs +const fs = require('fs') + +class Fs +{ + /** + * @param {string} wd Working directory + * @param {string} cli Command line interface + */ + constructor(wd, cli) + { + this.wd = wd + this.cli = cli + } + + mkdir(dir) + { + this.cli.write(' Creating path: ' + dir, 'green') + const path = this.wd + '/' + dir + fs.mkdirSync(path, { recursive: true }) + } + + writeFile(file, content) + { + const + path = this.wd + '/' + file, + file_exists = fs.existsSync(path) + + if(file_exists) + { + this.cli.write(' Existing file: ' + file, 'red') + } + else + { + this.cli.write(' Creating file: ' + file, 'green') + fs.writeFileSync(path, content) + } + } +} + +module.exports = Fs diff --git a/bin/superhero/generate-api-docs.js b/bin/superhero/generate-api-docs.js index 9d05c3d..280670f 100644 --- a/bin/superhero/generate-api-docs.js +++ b/bin/superhero/generate-api-docs.js @@ -1,39 +1,39 @@ -const -Fs = require('./fs'), -cwd = process.cwd(), -template_apiDocs = require('./file-template/api-docs') - -module.exports = async (cli) => -{ - cli.write(`Specify the path to where the project is located, or leave blank to use ${cwd}`) - const use_wd = await cli.question(`Where is the project root located?`) || cwd - cli.write(` ✔ Excellent\n`, 'green') - - const api_config = require(use_wd + '/src/api/config') - - if(!api_config - || !api_config.core - || !api_config.core.http - || !api_config.core.http.server - || !api_config.core.http.server.routes) - { - cli.write(`No routes could be located in the API configuration file, expected path: "core.http.server.routes"`, 'red') - return - } - - const routes = api_config.core.http.server.routes - - cli.write(' -------------', 'blue') - cli.write(' ¡ Finish it !', 'blue') - cli.write(' -------------', 'blue') - - const - fs = new Fs(use_wd, cli), - schemas = require(use_wd + '/src/domain/config').core.schema.composer, - docs = template_apiDocs(use_wd, routes, schemas) - - fs.mkdir('doc/') - fs.writeFile('doc/api-documentation.html', docs) - - cli.write(' -------------\n', 'blue') -} +const +Fs = require('./fs'), +cwd = process.cwd(), +template_apiDocs = require('./file-template/api-docs') + +module.exports = async (cli) => +{ + cli.write(`Specify the path to where the project is located, or leave blank to use ${cwd}`) + const use_wd = await cli.question(`Where is the project root located?`) || cwd + cli.write(` ✔ Excellent\n`, 'green') + + const api_config = require(use_wd + '/src/api/config') + + if(!api_config + || !api_config.core + || !api_config.core.http + || !api_config.core.http.server + || !api_config.core.http.server.routes) + { + cli.write(`No routes could be located in the API configuration file, expected path: "core.http.server.routes"`, 'red') + return + } + + const routes = api_config.core.http.server.routes + + cli.write(' -------------', 'blue') + cli.write(' ¡ Finish it !', 'blue') + cli.write(' -------------', 'blue') + + const + fs = new Fs(use_wd, cli), + schemas = require(use_wd + '/src/domain/config').core.schema.composer, + docs = template_apiDocs(use_wd, routes, schemas) + + fs.mkdir('doc/') + fs.writeFile('doc/api-documentation.html', docs) + + cli.write(' -------------\n', 'blue') +} diff --git a/bin/superhero/generate-api.js b/bin/superhero/generate-api.js index c8020d6..c9bc559 100644 --- a/bin/superhero/generate-api.js +++ b/bin/superhero/generate-api.js @@ -1,64 +1,64 @@ -const -CoreString = require('../../core/string'), -coreString = new CoreString, -cwd = process.cwd(), -path = require('path'), -template_test = require('./file-template/test-endpoint'), -template_dispatcher = require('./file-template/dispatcher'), -Fs = require('./fs') - -module.exports = async (cli) => -{ - cli.write(`Specify the path to where the project is located, or leave blank to use ${cwd}`) - const use_wd = await cli.question(`Where is the project root located?`) || cwd - cli.write(` ✔ Excellent\n`, 'green') - - const api_config = require(use_wd + '/src/api/config') - - if(!api_config - || !api_config.core - || !api_config.core.http - || !api_config.core.http.server - || !api_config.core.http.server.routes) - { - cli.write(`No routes could be located in the API configuration file, expected path: "core.http.server.routes"`, 'red') - return - } - - const routes = api_config.core.http.server.routes - - cli.write(' -------------', 'blue') - cli.write(' ¡ Finish it !', 'blue') - cli.write(' -------------', 'blue') - - const fs = new Fs(use_wd, cli) - - for(const endpoint in routes) - { - if('endpoint' in routes[endpoint]) - { - const - endpoint_name = coreString.composeCamelCase(endpoint), - fileEndpointPath = 'src/' + routes[endpoint].endpoint + '.js', - fileEndpointPathDir = path.dirname(fileEndpointPath), - fileEndpointContent = template_dispatcher(endpoint_name) - - fs.mkdir(fileEndpointPathDir) - fs.writeFile(fileEndpointPath, fileEndpointContent) - - const - fileTestPath = 'test/endpoint/' + endpoint + '.test.js', - fileTestPathDir = path.dirname(fileTestPath), - fileTestContent = template_test(endpoint) - - fs.mkdir(fileTestPathDir) - fs.writeFile(fileTestPath, fileTestContent) - } - else - { - cli.write(` Test generation requires a missing endpoint defintion in route "${endpoint}"`, 'red') - } - } - - cli.write(' -------------\n', 'blue') -} +const +CoreString = require('../../core/string'), +coreString = new CoreString, +cwd = process.cwd(), +path = require('path'), +template_test = require('./file-template/test-endpoint'), +template_dispatcher = require('./file-template/dispatcher'), +Fs = require('./fs') + +module.exports = async (cli) => +{ + cli.write(`Specify the path to where the project is located, or leave blank to use ${cwd}`) + const use_wd = await cli.question(`Where is the project root located?`) || cwd + cli.write(` ✔ Excellent\n`, 'green') + + const api_config = require(use_wd + '/src/api/config') + + if(!api_config + || !api_config.core + || !api_config.core.http + || !api_config.core.http.server + || !api_config.core.http.server.routes) + { + cli.write(`No routes could be located in the API configuration file, expected path: "core.http.server.routes"`, 'red') + return + } + + const routes = api_config.core.http.server.routes + + cli.write(' -------------', 'blue') + cli.write(' ¡ Finish it !', 'blue') + cli.write(' -------------', 'blue') + + const fs = new Fs(use_wd, cli) + + for(const endpoint in routes) + { + if('endpoint' in routes[endpoint]) + { + const + endpoint_name = coreString.composeCamelCase(endpoint), + fileEndpointPath = 'src/' + routes[endpoint].endpoint + '.js', + fileEndpointPathDir = path.dirname(fileEndpointPath), + fileEndpointContent = template_dispatcher(endpoint_name) + + fs.mkdir(fileEndpointPathDir) + fs.writeFile(fileEndpointPath, fileEndpointContent) + + const + fileTestPath = 'test/endpoint/' + endpoint + '.test.js', + fileTestPathDir = path.dirname(fileTestPath), + fileTestContent = template_test(endpoint) + + fs.mkdir(fileTestPathDir) + fs.writeFile(fileTestPath, fileTestContent) + } + else + { + cli.write(` Test generation requires a missing endpoint defintion in route "${endpoint}"`, 'red') + } + } + + cli.write(' -------------\n', 'blue') +} diff --git a/bin/superhero/generate-domain.js b/bin/superhero/generate-domain.js index 60866b2..8ac254f 100644 --- a/bin/superhero/generate-domain.js +++ b/bin/superhero/generate-domain.js @@ -1,50 +1,50 @@ -const -Fs = require('./fs'), -path = require('path'), -cwd = process.cwd(), -CoreString = require('../../core/string'), -coreString = new CoreString, -template_schema = require('./file-template/schema') - -module.exports = async (cli) => -{ - cli.write(`Specify the path to where the project is located, or leave blank to use ${cwd}`) - const use_wd = await cli.question(`Where is the project root located?`) || cwd - cli.write(` ✔ Excellent\n`, 'green') - - const domain_config = require(use_wd + '/src/domain/config') - - if(!domain_config - || !domain_config.core - || !domain_config.core.schema - || !domain_config.core.schema.composer) - { - cli.write(`No schemas could be located for the composer in the domain configuration file, expected path: "core.schema.composer"`, 'red') - return - } - - const - schemas = domain_config.core.schema.composer, - fs = new Fs('', cli) - - cli.write(' -------------', 'blue') - cli.write(' ¡ Finish it !', 'blue') - cli.write(' -------------', 'blue') - - for(const schema in schemas) - { - let schema_name - schema_name = coreString.composeCamelCase(schema) - schema_name = coreString.composeFirstUpperCase(schema_name) - - const - fileSchemaPath = schemas[schema] + '.js', - fileSchemaPathDir = path.dirname(fileSchemaPath), - fileSchemaContent = template_schema(schema_name) - - fs.mkdir(fileSchemaPathDir) - fs.writeFile(fileSchemaPath, fileSchemaContent) - } - - cli.write(' -------------\n', 'blue') -} +const +Fs = require('./fs'), +path = require('path'), +cwd = process.cwd(), +CoreString = require('../../core/string'), +coreString = new CoreString, +template_schema = require('./file-template/schema') + +module.exports = async (cli) => +{ + cli.write(`Specify the path to where the project is located, or leave blank to use ${cwd}`) + const use_wd = await cli.question(`Where is the project root located?`) || cwd + cli.write(` ✔ Excellent\n`, 'green') + + const domain_config = require(use_wd + '/src/domain/config') + + if(!domain_config + || !domain_config.core + || !domain_config.core.schema + || !domain_config.core.schema.composer) + { + cli.write(`No schemas could be located for the composer in the domain configuration file, expected path: "core.schema.composer"`, 'red') + return + } + + const + schemas = domain_config.core.schema.composer, + fs = new Fs('', cli) + + cli.write(' -------------', 'blue') + cli.write(' ¡ Finish it !', 'blue') + cli.write(' -------------', 'blue') + + for(const schema in schemas) + { + let schema_name + schema_name = coreString.composeCamelCase(schema) + schema_name = coreString.composeFirstUpperCase(schema_name) + + const + fileSchemaPath = schemas[schema] + '.js', + fileSchemaPathDir = path.dirname(fileSchemaPath), + fileSchemaContent = template_schema(schema_name) + + fs.mkdir(fileSchemaPathDir) + fs.writeFile(fileSchemaPath, fileSchemaContent) + } + + cli.write(' -------------\n', 'blue') +} diff --git a/bin/superhero/generate-project.js b/bin/superhero/generate-project.js index ace57e3..1eed6c0 100644 --- a/bin/superhero/generate-project.js +++ b/bin/superhero/generate-project.js @@ -1,76 +1,76 @@ -const -CoreString = require('../../core/string'), -coreString = new CoreString, -cwd = process.cwd(), -version = require(__dirname + '/../../package.json').version, -template_config = require('./file-template/config'), -template_config_api = require('./file-template/config-api'), -template_config_domain = require('./file-template/config-domain'), -template_config_infrastructure = require('./file-template/config-infrastructure'), -template_testInit = require('./file-template/test-init'), -template_gitignore = require('./file-template/gitignore'), -template_packageJson = require('./file-template/package-json'), -template_dockerfile = require('./file-template/dockerfile'), -template_readme = require('./file-template/readme-md'), -template_main = require('./file-template/main'), -Fs = require('./fs'), -timezones = require('./timezones') - -module.exports = async (cli) => -{ - const use_name = await cli.question(`What is the name of the project?`) - const use_name_dashed = coreString.composeSeperatedLowerCase(use_name) - cli.write(` ✔ Excellent\n`, 'green') - const use_description = await cli.question(`How would you describe the project in one sentence?`) - cli.write(` ✔ Excellent\n`, 'green') - const use_repository = await cli.question(`What is the URL to the repository?`) - cli.write(` ✔ Excellent\n`, 'green') - const use_infrastructure = await cli.question(`Add "infrastructure" component?`, ['yes', 'no']) - cli.write(` ✔ Excellent\n`, 'green') - const use_view = await cli.question(`Add "view" component?`, ['yes', 'no']) - cli.write(` ✔ Excellent\n`, 'green') - const use_timezone = await cli.question(`What timezone does the project reflect?`, timezones) - cli.write(` ✔ Excellent\n`, 'green') - - // working directory - cli.write(`Specify the path to where the project will be generated, or leave blank to use ${cwd}/${use_name_dashed}`) - let use_wd - use_wd = await cli.question(`Where do you want to generate the project?`) - use_wd = use_wd || cwd + '/' + use_name_dashed - cli.write(` ✔ Excellent\n`, 'green') - - const fs = new Fs(use_wd, cli) - - cli.write(' -------------', 'blue') - cli.write(' ¡ Finish it !', 'blue') - cli.write(' -------------', 'blue') - - fs.mkdir('src') - fs.mkdir('src/api') - fs.mkdir('src/domain') - fs.mkdir('test') - - fs.writeFile('src/api/config.js', template_config_api('Api')) - fs.writeFile('src/domain/config.js', template_config_domain('Domain')) - - use_infrastructure === 'yes' - && fs.mkdir('src/infrastructure') - - use_infrastructure === 'yes' - && fs.writeFile('src/infrastructure/config.js', template_config_infrastructure('Infrastructure')) - - use_view === 'yes' - && fs.mkdir('src/view') - - use_view === 'yes' - && fs.writeFile('src/view/config.js', template_config('View')) - - fs.writeFile('src/index.js', template_main(use_infrastructure, use_view)) - fs.writeFile('test/init.js', template_testInit()) - fs.writeFile('.gitignore', template_gitignore()) - fs.writeFile('Dockerfile', template_dockerfile(use_timezone)) - fs.writeFile('package.json', template_packageJson(use_name_dashed, use_description, use_repository, version)) - fs.writeFile('README.md', template_readme(use_name)) - - cli.write(' -------------\n', 'blue') -} +const +CoreString = require('../../core/string'), +coreString = new CoreString, +cwd = process.cwd(), +version = require(__dirname + '/../../package.json').version, +template_config = require('./file-template/config'), +template_config_api = require('./file-template/config-api'), +template_config_domain = require('./file-template/config-domain'), +template_config_infrastructure = require('./file-template/config-infrastructure'), +template_testInit = require('./file-template/test-init'), +template_gitignore = require('./file-template/gitignore'), +template_packageJson = require('./file-template/package-json'), +template_dockerfile = require('./file-template/dockerfile'), +template_readme = require('./file-template/readme-md'), +template_main = require('./file-template/main'), +Fs = require('./fs'), +timezones = require('./timezones') + +module.exports = async (cli) => +{ + const use_name = await cli.question(`What is the name of the project?`) + const use_name_dashed = coreString.composeSeperatedLowerCase(use_name) + cli.write(` ✔ Excellent\n`, 'green') + const use_description = await cli.question(`How would you describe the project in one sentence?`) + cli.write(` ✔ Excellent\n`, 'green') + const use_repository = await cli.question(`What is the URL to the repository?`) + cli.write(` ✔ Excellent\n`, 'green') + const use_infrastructure = await cli.question(`Add "infrastructure" component?`, ['yes', 'no']) + cli.write(` ✔ Excellent\n`, 'green') + const use_view = await cli.question(`Add "view" component?`, ['yes', 'no']) + cli.write(` ✔ Excellent\n`, 'green') + const use_timezone = await cli.question(`What timezone does the project reflect?`, timezones) + cli.write(` ✔ Excellent\n`, 'green') + + // working directory + cli.write(`Specify the path to where the project will be generated, or leave blank to use ${cwd}/${use_name_dashed}`) + let use_wd + use_wd = await cli.question(`Where do you want to generate the project?`) + use_wd = use_wd || cwd + '/' + use_name_dashed + cli.write(` ✔ Excellent\n`, 'green') + + const fs = new Fs(use_wd, cli) + + cli.write(' -------------', 'blue') + cli.write(' ¡ Finish it !', 'blue') + cli.write(' -------------', 'blue') + + fs.mkdir('src') + fs.mkdir('src/api') + fs.mkdir('src/domain') + fs.mkdir('test') + + fs.writeFile('src/api/config.js', template_config_api('Api')) + fs.writeFile('src/domain/config.js', template_config_domain('Domain')) + + use_infrastructure === 'yes' + && fs.mkdir('src/infrastructure') + + use_infrastructure === 'yes' + && fs.writeFile('src/infrastructure/config.js', template_config_infrastructure('Infrastructure')) + + use_view === 'yes' + && fs.mkdir('src/view') + + use_view === 'yes' + && fs.writeFile('src/view/config.js', template_config('View')) + + fs.writeFile('src/index.js', template_main(use_infrastructure, use_view)) + fs.writeFile('test/init.js', template_testInit()) + fs.writeFile('.gitignore', template_gitignore()) + fs.writeFile('Dockerfile', template_dockerfile(use_timezone)) + fs.writeFile('package.json', template_packageJson(use_name_dashed, use_description, use_repository, version)) + fs.writeFile('README.md', template_readme(use_name)) + + cli.write(' -------------\n', 'blue') +} diff --git a/bin/superhero/timezones/index.js b/bin/superhero/timezones/index.js index 87f5c6b..02befe0 100644 --- a/bin/superhero/timezones/index.js +++ b/bin/superhero/timezones/index.js @@ -1,593 +1,593 @@ -module.exports = -[ - 'Africa/Abidjan', - 'Africa/Accra', - 'Africa/Addis_Ababa', - 'Africa/Algiers', - 'Africa/Asmara', - 'Africa/Bamako', - 'Africa/Bangui', - 'Africa/Banjul', - 'Africa/Bissau', - 'Africa/Blantyre', - 'Africa/Brazzaville', - 'Africa/Bujumbura', - 'Africa/Cairo', - 'Africa/Casablanca', - 'Africa/Ceuta', - 'Africa/Conakry', - 'Africa/Dakar', - 'Africa/Dar_es_Salaam', - 'Africa/Djibouti', - 'Africa/Douala', - 'Africa/El_Aaiun', - 'Africa/Freetown', - 'Africa/Gaborone', - 'Africa/Harare', - 'Africa/Johannesburg', - 'Africa/Juba', - 'Africa/Kampala', - 'Africa/Khartoum', - 'Africa/Kigali', - 'Africa/Kinshasa', - 'Africa/Lagos', - 'Africa/Libreville', - 'Africa/Lome', - 'Africa/Luanda', - 'Africa/Lubumbashi', - 'Africa/Lusaka', - 'Africa/Malabo', - 'Africa/Maputo', - 'Africa/Maseru', - 'Africa/Mbabane', - 'Africa/Mogadishu', - 'Africa/Monrovia', - 'Africa/Nairobi', - 'Africa/Ndjamena', - 'Africa/Niamey', - 'Africa/Nouakchott', - 'Africa/Ouagadougou', - 'Africa/Porto-Novo', - 'Africa/Sao_Tome', - 'Africa/Timbuktu', - 'Africa/Tripoli', - 'Africa/Tunis', - 'Africa/Windhoek', - 'America/Adak', - 'America/Anchorage', - 'America/Anguilla', - 'America/Antigua', - 'America/Araguaina', - 'America/Argentina/Buenos_Aires', - 'America/Argentina/Catamarca', - 'America/Argentina/ComodRivadavia', - 'America/Argentina/Cordoba', - 'America/Argentina/Jujuy', - 'America/Argentina/La_Rioja', - 'America/Argentina/Mendoza', - 'America/Argentina/Rio_Gallegos', - 'America/Argentina/Salta', - 'America/Argentina/San_Juan', - 'America/Argentina/San_Luis', - 'America/Argentina/Tucuman', - 'America/Argentina/Ushuaia', - 'America/Aruba', - 'America/Asuncion', - 'America/Atikokan', - 'America/Atka', - 'America/Bahia', - 'America/Bahia_Banderas', - 'America/Barbados', - 'America/Belem', - 'America/Belize', - 'America/Blanc-Sablon', - 'America/Boa_Vista', - 'America/Bogota', - 'America/Boise', - 'America/Buenos_Aires', - 'America/Cambridge_Bay', - 'America/Campo_Grande', - 'America/Cancun', - 'America/Caracas', - 'America/Catamarca', - 'America/Cayenne', - 'America/Cayman', - 'America/Chicago', - 'America/Chihuahua', - 'America/Coral_Harbour', - 'America/Cordoba', - 'America/Costa_Rica', - 'America/Creston', - 'America/Cuiaba', - 'America/Curacao', - 'America/Danmarkshavn', - 'America/Dawson', - 'America/Dawson_Creek', - 'America/Denver', - 'America/Detroit', - 'America/Dominica', - 'America/Edmonton', - 'America/Eirunepe', - 'America/El_Salvador', - 'America/Ensenada', - 'America/Fort_Nelson', - 'America/Fort_Wayne', - 'America/Fortaleza', - 'America/Glace_Bay', - 'America/Godthab', - 'America/Goose_Bay', - 'America/Grand_Turk', - 'America/Grenada', - 'America/Guadeloupe', - 'America/Guatemala', - 'America/Guayaquil', - 'America/Guyana', - 'America/Halifax', - 'America/Havana', - 'America/Hermosillo', - 'America/Indiana/Indianapolis', - 'America/Indiana/Knox', - 'America/Indiana/Marengo', - 'America/Indiana/Petersburg', - 'America/Indiana/Tell_City', - 'America/Indiana/Vevay', - 'America/Indiana/Vincennes', - 'America/Indiana/Winamac', - 'America/Indianapolis', - 'America/Inuvik', - 'America/Iqaluit', - 'America/Jamaica', - 'America/Jujuy', - 'America/Juneau', - 'America/Kentucky/Louisville', - 'America/Kentucky/Monticello', - 'America/Knox_IN', - 'America/Kralendijk', - 'America/La_Paz', - 'America/Lima', - 'America/Los_Angeles', - 'America/Louisville', - 'America/Lower_Princes', - 'America/Maceio', - 'America/Managua', - 'America/Manaus', - 'America/Marigot', - 'America/Martinique', - 'America/Matamoros', - 'America/Mazatlan', - 'America/Mendoza', - 'America/Menominee', - 'America/Merida', - 'America/Metlakatla', - 'America/Mexico_City', - 'America/Miquelon', - 'America/Moncton', - 'America/Monterrey', - 'America/Montevideo', - 'America/Montreal', - 'America/Montserrat', - 'America/Nassau', - 'America/New_York', - 'America/Nipigon', - 'America/Nome', - 'America/Noronha', - 'America/North_Dakota/Beulah', - 'America/North_Dakota/Center', - 'America/North_Dakota/New_Salem', - 'America/Ojinaga', - 'America/Panama', - 'America/Pangnirtung', - 'America/Paramaribo', - 'America/Phoenix', - 'America/Port_of_Spain', - 'America/Port-au-Prince', - 'America/Porto_Acre', - 'America/Porto_Velho', - 'America/Puerto_Rico', - 'America/Punta_Arenas', - 'America/Rainy_River', - 'America/Rankin_Inlet', - 'America/Recife', - 'America/Regina', - 'America/Resolute', - 'America/Rio_Branco', - 'America/Rosario', - 'America/Santa_Isabel', - 'America/Santarem', - 'America/Santiago', - 'America/Santo_Domingo', - 'America/Sao_Paulo', - 'America/Scoresbysund', - 'America/Shiprock', - 'America/Sitka', - 'America/St_Barthelemy', - 'America/St_Johns', - 'America/St_Kitts', - 'America/St_Lucia', - 'America/St_Thomas', - 'America/St_Vincent', - 'America/Swift_Current', - 'America/Tegucigalpa', - 'America/Thule', - 'America/Thunder_Bay', - 'America/Tijuana', - 'America/Toronto', - 'America/Tortola', - 'America/Vancouver', - 'America/Virgin', - 'America/Whitehorse', - 'America/Winnipeg', - 'America/Yakutat', - 'America/Yellowknife', - 'Antarctica/Casey', - 'Antarctica/Davis', - 'Antarctica/DumontDUrville', - 'Antarctica/Macquarie', - 'Antarctica/Mawson', - 'Antarctica/McMurdo', - 'Antarctica/Palmer', - 'Antarctica/Rothera', - 'Antarctica/South_Pole', - 'Antarctica/Syowa', - 'Antarctica/Troll', - 'Antarctica/Vostok', - 'Arctic/Longyearbyen', - 'Asia/Aden', - 'Asia/Almaty', - 'Asia/Amman', - 'Asia/Anadyr', - 'Asia/Aqtau', - 'Asia/Aqtobe', - 'Asia/Ashgabat', - 'Asia/Ashkhabad', - 'Asia/Atyrau', - 'Asia/Baghdad', - 'Asia/Bahrain', - 'Asia/Baku', - 'Asia/Bangkok', - 'Asia/Barnaul', - 'Asia/Beirut', - 'Asia/Bishkek', - 'Asia/Brunei', - 'Asia/Calcutta', - 'Asia/Chita', - 'Asia/Choibalsan', - 'Asia/Chongqing', - 'Asia/Chungking', - 'Asia/Colombo', - 'Asia/Dacca', - 'Asia/Damascus', - 'Asia/Dhaka', - 'Asia/Dili', - 'Asia/Dubai', - 'Asia/Dushanbe', - 'Asia/Famagusta', - 'Asia/Gaza', - 'Asia/Harbin', - 'Asia/Hebron', - 'Asia/Ho_Chi_Minh', - 'Asia/Hong_Kong', - 'Asia/Hovd', - 'Asia/Irkutsk', - 'Asia/Istanbul', - 'Asia/Jakarta', - 'Asia/Jayapura', - 'Asia/Jerusalem', - 'Asia/Kabul', - 'Asia/Kamchatka', - 'Asia/Karachi', - 'Asia/Kashgar', - 'Asia/Kathmandu', - 'Asia/Katmandu', - 'Asia/Khandyga', - 'Asia/Kolkata', - 'Asia/Krasnoyarsk', - 'Asia/Kuala_Lumpur', - 'Asia/Kuching', - 'Asia/Kuwait', - 'Asia/Macao', - 'Asia/Macau', - 'Asia/Magadan', - 'Asia/Makassar', - 'Asia/Manila', - 'Asia/Muscat', - 'Asia/Novokuznetsk', - 'Asia/Novosibirsk', - 'Asia/Omsk', - 'Asia/Oral', - 'Asia/Phnom_Penh', - 'Asia/Pontianak', - 'Asia/Pyongyang', - 'Asia/Qatar', - 'Asia/Qyzylorda', - 'Asia/Rangoon', - 'Asia/Riyadh', - 'Asia/Saigon', - 'Asia/Sakhalin', - 'Asia/Samarkand', - 'Asia/Seoul', - 'Asia/Shanghai', - 'Asia/Singapore', - 'Asia/Srednekolymsk', - 'Asia/Taipei', - 'Asia/Tashkent', - 'Asia/Tbilisi', - 'Asia/Tehran', - 'Asia/Tel_Aviv', - 'Asia/Thimbu', - 'Asia/Thimphu', - 'Asia/Tokyo', - 'Asia/Tomsk', - 'Asia/Ujung_Pandang', - 'Asia/Ulaanbaatar', - 'Asia/Ulan_Bator', - 'Asia/Urumqi', - 'Asia/Ust-Nera', - 'Asia/Vientiane', - 'Asia/Vladivostok', - 'Asia/Yakutsk', - 'Asia/Yangon', - 'Asia/Yekaterinburg', - 'Asia/Yerevan', - 'Atlantic/Azores', - 'Atlantic/Bermuda', - 'Atlantic/Canary', - 'Atlantic/Cape_Verde', - 'Atlantic/Faeroe', - 'Atlantic/Faroe', - 'Atlantic/Jan_Mayen', - 'Atlantic/Madeira', - 'Atlantic/Reykjavik', - 'Atlantic/South_Georgia', - 'Atlantic/St_Helena', - 'Atlantic/Stanley', - 'Australia/ACT', - 'Australia/Adelaide', - 'Australia/Brisbane', - 'Australia/Broken_Hill', - 'Australia/Canberra', - 'Australia/Currie', - 'Australia/Darwin', - 'Australia/Eucla', - 'Australia/Hobart', - 'Australia/LHI', - 'Australia/Lindeman', - 'Australia/Lord_Howe', - 'Australia/Melbourne', - 'Australia/North', - 'Australia/NSW', - 'Australia/Perth', - 'Australia/Queensland', - 'Australia/South', - 'Australia/Sydney', - 'Australia/Tasmania', - 'Australia/Victoria', - 'Australia/West', - 'Australia/Yancowinna', - 'Brazil/Acre', - 'Brazil/DeNoronha', - 'Brazil/East', - 'Brazil/West', - 'Canada/Atlantic', - 'Canada/Central', - 'Canada/Eastern', - 'Canada/Mountain', - 'Canada/Newfoundland', - 'Canada/Pacific', - 'Canada/Saskatchewan', - 'Canada/Yukon', - 'CET', - 'Chile/Continental', - 'Chile/EasterIsland', - 'CST6CDT', - 'Cuba', - 'EET', - 'Egypt', - 'Eire', - 'EST', - 'EST5EDT', - 'Etc/GMT', - 'Etc/GMT+0', - 'Etc/GMT+1', - 'Etc/GMT+10', - 'Etc/GMT+11', - 'Etc/GMT+12', - 'Etc/GMT+2', - 'Etc/GMT+3', - 'Etc/GMT+4', - 'Etc/GMT+5', - 'Etc/GMT+6', - 'Etc/GMT+7', - 'Etc/GMT+8', - 'Etc/GMT+9', - 'Etc/GMT0', - 'Etc/GMT-0', - 'Etc/GMT-1', - 'Etc/GMT-10', - 'Etc/GMT-11', - 'Etc/GMT-12', - 'Etc/GMT-13', - 'Etc/GMT-14', - 'Etc/GMT-2', - 'Etc/GMT-3', - 'Etc/GMT-4', - 'Etc/GMT-5', - 'Etc/GMT-6', - 'Etc/GMT-7', - 'Etc/GMT-8', - 'Etc/GMT-9', - 'Etc/Greenwich', - 'Etc/UCT', - 'Etc/Universal', - 'Etc/UTC', - 'Etc/Zulu', - 'Europe/Amsterdam', - 'Europe/Andorra', - 'Europe/Astrakhan', - 'Europe/Athens', - 'Europe/Belfast', - 'Europe/Belgrade', - 'Europe/Berlin', - 'Europe/Bratislava', - 'Europe/Brussels', - 'Europe/Bucharest', - 'Europe/Budapest', - 'Europe/Busingen', - 'Europe/Chisinau', - 'Europe/Copenhagen', - 'Europe/Dublin', - 'Europe/Gibraltar', - 'Europe/Guernsey', - 'Europe/Helsinki', - 'Europe/Isle_of_Man', - 'Europe/Istanbul', - 'Europe/Jersey', - 'Europe/Kaliningrad', - 'Europe/Kiev', - 'Europe/Kirov', - 'Europe/Lisbon', - 'Europe/Ljubljana', - 'Europe/London', - 'Europe/Luxembourg', - 'Europe/Madrid', - 'Europe/Malta', - 'Europe/Mariehamn', - 'Europe/Minsk', - 'Europe/Monaco', - 'Europe/Moscow', - 'Asia/Nicosia', - 'Europe/Oslo', - 'Europe/Paris', - 'Europe/Podgorica', - 'Europe/Prague', - 'Europe/Riga', - 'Europe/Rome', - 'Europe/Samara', - 'Europe/San_Marino', - 'Europe/Sarajevo', - 'Europe/Saratov', - 'Europe/Simferopol', - 'Europe/Skopje', - 'Europe/Sofia', - 'Europe/Stockholm', - 'Europe/Tallinn', - 'Europe/Tirane', - 'Europe/Tiraspol', - 'Europe/Ulyanovsk', - 'Europe/Uzhgorod', - 'Europe/Vaduz', - 'Europe/Vatican', - 'Europe/Vienna', - 'Europe/Vilnius', - 'Europe/Volgograd', - 'Europe/Warsaw', - 'Europe/Zagreb', - 'Europe/Zaporozhye', - 'Europe/Zurich', - 'GB', - 'GB-Eire', - 'GMT', - 'GMT+0', - 'GMT0', - 'GMT−0', - 'Greenwich', - 'Hongkong', - 'HST', - 'Iceland', - 'Indian/Antananarivo', - 'Indian/Chagos', - 'Indian/Christmas', - 'Indian/Cocos', - 'Indian/Comoro', - 'Indian/Kerguelen', - 'Indian/Mahe', - 'Indian/Maldives', - 'Indian/Mauritius', - 'Indian/Mayotte', - 'Indian/Reunion', - 'Iran', - 'Israel', - 'Jamaica', - 'Japan', - 'Kwajalein', - 'Libya', - 'MET', - 'Mexico/BajaNorte', - 'Mexico/BajaSur', - 'Mexico/General', - 'MST', - 'MST7MDT', - 'Navajo', - 'NZ', - 'NZ-CHAT', - 'Pacific/Apia', - 'Pacific/Auckland', - 'Pacific/Bougainville', - 'Pacific/Chatham', - 'Pacific/Chuuk', - 'Pacific/Easter', - 'Pacific/Efate', - 'Pacific/Enderbury', - 'Pacific/Fakaofo', - 'Pacific/Fiji', - 'Pacific/Funafuti', - 'Pacific/Galapagos', - 'Pacific/Gambier', - 'Pacific/Guadalcanal', - 'Pacific/Guam', - 'Pacific/Honolulu', - 'Pacific/Johnston', - 'Pacific/Kiritimati', - 'Pacific/Kosrae', - 'Pacific/Kwajalein', - 'Pacific/Majuro', - 'Pacific/Marquesas', - 'Pacific/Midway', - 'Pacific/Nauru', - 'Pacific/Niue', - 'Pacific/Norfolk', - 'Pacific/Noumea', - 'Pacific/Pago_Pago', - 'Pacific/Palau', - 'Pacific/Pitcairn', - 'Pacific/Pohnpei', - 'Pacific/Ponape', - 'Pacific/Port_Moresby', - 'Pacific/Rarotonga', - 'Pacific/Saipan', - 'Pacific/Samoa', - 'Pacific/Tahiti', - 'Pacific/Tarawa', - 'Pacific/Tongatapu', - 'Pacific/Truk', - 'Pacific/Wake', - 'Pacific/Wallis', - 'Pacific/Yap', - 'Poland', - 'Portugal', - 'PRC', - 'PST8PDT', - 'ROC', - 'ROK', - 'Singapore', - 'Turkey', - 'UCT', - 'Universal', - 'US/Alaska', - 'US/Aleutian', - 'US/Arizona', - 'US/Central', - 'US/Eastern', - 'US/East-Indiana', - 'US/Hawaii', - 'US/Indiana-Starke', - 'US/Michigan', - 'US/Mountain', - 'US/Pacific', - 'US/Pacific-New', - 'US/Samoa', - 'UTC', - 'WET', - 'W-SU', - 'Zulu' -] +module.exports = +[ + 'Africa/Abidjan', + 'Africa/Accra', + 'Africa/Addis_Ababa', + 'Africa/Algiers', + 'Africa/Asmara', + 'Africa/Bamako', + 'Africa/Bangui', + 'Africa/Banjul', + 'Africa/Bissau', + 'Africa/Blantyre', + 'Africa/Brazzaville', + 'Africa/Bujumbura', + 'Africa/Cairo', + 'Africa/Casablanca', + 'Africa/Ceuta', + 'Africa/Conakry', + 'Africa/Dakar', + 'Africa/Dar_es_Salaam', + 'Africa/Djibouti', + 'Africa/Douala', + 'Africa/El_Aaiun', + 'Africa/Freetown', + 'Africa/Gaborone', + 'Africa/Harare', + 'Africa/Johannesburg', + 'Africa/Juba', + 'Africa/Kampala', + 'Africa/Khartoum', + 'Africa/Kigali', + 'Africa/Kinshasa', + 'Africa/Lagos', + 'Africa/Libreville', + 'Africa/Lome', + 'Africa/Luanda', + 'Africa/Lubumbashi', + 'Africa/Lusaka', + 'Africa/Malabo', + 'Africa/Maputo', + 'Africa/Maseru', + 'Africa/Mbabane', + 'Africa/Mogadishu', + 'Africa/Monrovia', + 'Africa/Nairobi', + 'Africa/Ndjamena', + 'Africa/Niamey', + 'Africa/Nouakchott', + 'Africa/Ouagadougou', + 'Africa/Porto-Novo', + 'Africa/Sao_Tome', + 'Africa/Timbuktu', + 'Africa/Tripoli', + 'Africa/Tunis', + 'Africa/Windhoek', + 'America/Adak', + 'America/Anchorage', + 'America/Anguilla', + 'America/Antigua', + 'America/Araguaina', + 'America/Argentina/Buenos_Aires', + 'America/Argentina/Catamarca', + 'America/Argentina/ComodRivadavia', + 'America/Argentina/Cordoba', + 'America/Argentina/Jujuy', + 'America/Argentina/La_Rioja', + 'America/Argentina/Mendoza', + 'America/Argentina/Rio_Gallegos', + 'America/Argentina/Salta', + 'America/Argentina/San_Juan', + 'America/Argentina/San_Luis', + 'America/Argentina/Tucuman', + 'America/Argentina/Ushuaia', + 'America/Aruba', + 'America/Asuncion', + 'America/Atikokan', + 'America/Atka', + 'America/Bahia', + 'America/Bahia_Banderas', + 'America/Barbados', + 'America/Belem', + 'America/Belize', + 'America/Blanc-Sablon', + 'America/Boa_Vista', + 'America/Bogota', + 'America/Boise', + 'America/Buenos_Aires', + 'America/Cambridge_Bay', + 'America/Campo_Grande', + 'America/Cancun', + 'America/Caracas', + 'America/Catamarca', + 'America/Cayenne', + 'America/Cayman', + 'America/Chicago', + 'America/Chihuahua', + 'America/Coral_Harbour', + 'America/Cordoba', + 'America/Costa_Rica', + 'America/Creston', + 'America/Cuiaba', + 'America/Curacao', + 'America/Danmarkshavn', + 'America/Dawson', + 'America/Dawson_Creek', + 'America/Denver', + 'America/Detroit', + 'America/Dominica', + 'America/Edmonton', + 'America/Eirunepe', + 'America/El_Salvador', + 'America/Ensenada', + 'America/Fort_Nelson', + 'America/Fort_Wayne', + 'America/Fortaleza', + 'America/Glace_Bay', + 'America/Godthab', + 'America/Goose_Bay', + 'America/Grand_Turk', + 'America/Grenada', + 'America/Guadeloupe', + 'America/Guatemala', + 'America/Guayaquil', + 'America/Guyana', + 'America/Halifax', + 'America/Havana', + 'America/Hermosillo', + 'America/Indiana/Indianapolis', + 'America/Indiana/Knox', + 'America/Indiana/Marengo', + 'America/Indiana/Petersburg', + 'America/Indiana/Tell_City', + 'America/Indiana/Vevay', + 'America/Indiana/Vincennes', + 'America/Indiana/Winamac', + 'America/Indianapolis', + 'America/Inuvik', + 'America/Iqaluit', + 'America/Jamaica', + 'America/Jujuy', + 'America/Juneau', + 'America/Kentucky/Louisville', + 'America/Kentucky/Monticello', + 'America/Knox_IN', + 'America/Kralendijk', + 'America/La_Paz', + 'America/Lima', + 'America/Los_Angeles', + 'America/Louisville', + 'America/Lower_Princes', + 'America/Maceio', + 'America/Managua', + 'America/Manaus', + 'America/Marigot', + 'America/Martinique', + 'America/Matamoros', + 'America/Mazatlan', + 'America/Mendoza', + 'America/Menominee', + 'America/Merida', + 'America/Metlakatla', + 'America/Mexico_City', + 'America/Miquelon', + 'America/Moncton', + 'America/Monterrey', + 'America/Montevideo', + 'America/Montreal', + 'America/Montserrat', + 'America/Nassau', + 'America/New_York', + 'America/Nipigon', + 'America/Nome', + 'America/Noronha', + 'America/North_Dakota/Beulah', + 'America/North_Dakota/Center', + 'America/North_Dakota/New_Salem', + 'America/Ojinaga', + 'America/Panama', + 'America/Pangnirtung', + 'America/Paramaribo', + 'America/Phoenix', + 'America/Port_of_Spain', + 'America/Port-au-Prince', + 'America/Porto_Acre', + 'America/Porto_Velho', + 'America/Puerto_Rico', + 'America/Punta_Arenas', + 'America/Rainy_River', + 'America/Rankin_Inlet', + 'America/Recife', + 'America/Regina', + 'America/Resolute', + 'America/Rio_Branco', + 'America/Rosario', + 'America/Santa_Isabel', + 'America/Santarem', + 'America/Santiago', + 'America/Santo_Domingo', + 'America/Sao_Paulo', + 'America/Scoresbysund', + 'America/Shiprock', + 'America/Sitka', + 'America/St_Barthelemy', + 'America/St_Johns', + 'America/St_Kitts', + 'America/St_Lucia', + 'America/St_Thomas', + 'America/St_Vincent', + 'America/Swift_Current', + 'America/Tegucigalpa', + 'America/Thule', + 'America/Thunder_Bay', + 'America/Tijuana', + 'America/Toronto', + 'America/Tortola', + 'America/Vancouver', + 'America/Virgin', + 'America/Whitehorse', + 'America/Winnipeg', + 'America/Yakutat', + 'America/Yellowknife', + 'Antarctica/Casey', + 'Antarctica/Davis', + 'Antarctica/DumontDUrville', + 'Antarctica/Macquarie', + 'Antarctica/Mawson', + 'Antarctica/McMurdo', + 'Antarctica/Palmer', + 'Antarctica/Rothera', + 'Antarctica/South_Pole', + 'Antarctica/Syowa', + 'Antarctica/Troll', + 'Antarctica/Vostok', + 'Arctic/Longyearbyen', + 'Asia/Aden', + 'Asia/Almaty', + 'Asia/Amman', + 'Asia/Anadyr', + 'Asia/Aqtau', + 'Asia/Aqtobe', + 'Asia/Ashgabat', + 'Asia/Ashkhabad', + 'Asia/Atyrau', + 'Asia/Baghdad', + 'Asia/Bahrain', + 'Asia/Baku', + 'Asia/Bangkok', + 'Asia/Barnaul', + 'Asia/Beirut', + 'Asia/Bishkek', + 'Asia/Brunei', + 'Asia/Calcutta', + 'Asia/Chita', + 'Asia/Choibalsan', + 'Asia/Chongqing', + 'Asia/Chungking', + 'Asia/Colombo', + 'Asia/Dacca', + 'Asia/Damascus', + 'Asia/Dhaka', + 'Asia/Dili', + 'Asia/Dubai', + 'Asia/Dushanbe', + 'Asia/Famagusta', + 'Asia/Gaza', + 'Asia/Harbin', + 'Asia/Hebron', + 'Asia/Ho_Chi_Minh', + 'Asia/Hong_Kong', + 'Asia/Hovd', + 'Asia/Irkutsk', + 'Asia/Istanbul', + 'Asia/Jakarta', + 'Asia/Jayapura', + 'Asia/Jerusalem', + 'Asia/Kabul', + 'Asia/Kamchatka', + 'Asia/Karachi', + 'Asia/Kashgar', + 'Asia/Kathmandu', + 'Asia/Katmandu', + 'Asia/Khandyga', + 'Asia/Kolkata', + 'Asia/Krasnoyarsk', + 'Asia/Kuala_Lumpur', + 'Asia/Kuching', + 'Asia/Kuwait', + 'Asia/Macao', + 'Asia/Macau', + 'Asia/Magadan', + 'Asia/Makassar', + 'Asia/Manila', + 'Asia/Muscat', + 'Asia/Novokuznetsk', + 'Asia/Novosibirsk', + 'Asia/Omsk', + 'Asia/Oral', + 'Asia/Phnom_Penh', + 'Asia/Pontianak', + 'Asia/Pyongyang', + 'Asia/Qatar', + 'Asia/Qyzylorda', + 'Asia/Rangoon', + 'Asia/Riyadh', + 'Asia/Saigon', + 'Asia/Sakhalin', + 'Asia/Samarkand', + 'Asia/Seoul', + 'Asia/Shanghai', + 'Asia/Singapore', + 'Asia/Srednekolymsk', + 'Asia/Taipei', + 'Asia/Tashkent', + 'Asia/Tbilisi', + 'Asia/Tehran', + 'Asia/Tel_Aviv', + 'Asia/Thimbu', + 'Asia/Thimphu', + 'Asia/Tokyo', + 'Asia/Tomsk', + 'Asia/Ujung_Pandang', + 'Asia/Ulaanbaatar', + 'Asia/Ulan_Bator', + 'Asia/Urumqi', + 'Asia/Ust-Nera', + 'Asia/Vientiane', + 'Asia/Vladivostok', + 'Asia/Yakutsk', + 'Asia/Yangon', + 'Asia/Yekaterinburg', + 'Asia/Yerevan', + 'Atlantic/Azores', + 'Atlantic/Bermuda', + 'Atlantic/Canary', + 'Atlantic/Cape_Verde', + 'Atlantic/Faeroe', + 'Atlantic/Faroe', + 'Atlantic/Jan_Mayen', + 'Atlantic/Madeira', + 'Atlantic/Reykjavik', + 'Atlantic/South_Georgia', + 'Atlantic/St_Helena', + 'Atlantic/Stanley', + 'Australia/ACT', + 'Australia/Adelaide', + 'Australia/Brisbane', + 'Australia/Broken_Hill', + 'Australia/Canberra', + 'Australia/Currie', + 'Australia/Darwin', + 'Australia/Eucla', + 'Australia/Hobart', + 'Australia/LHI', + 'Australia/Lindeman', + 'Australia/Lord_Howe', + 'Australia/Melbourne', + 'Australia/North', + 'Australia/NSW', + 'Australia/Perth', + 'Australia/Queensland', + 'Australia/South', + 'Australia/Sydney', + 'Australia/Tasmania', + 'Australia/Victoria', + 'Australia/West', + 'Australia/Yancowinna', + 'Brazil/Acre', + 'Brazil/DeNoronha', + 'Brazil/East', + 'Brazil/West', + 'Canada/Atlantic', + 'Canada/Central', + 'Canada/Eastern', + 'Canada/Mountain', + 'Canada/Newfoundland', + 'Canada/Pacific', + 'Canada/Saskatchewan', + 'Canada/Yukon', + 'CET', + 'Chile/Continental', + 'Chile/EasterIsland', + 'CST6CDT', + 'Cuba', + 'EET', + 'Egypt', + 'Eire', + 'EST', + 'EST5EDT', + 'Etc/GMT', + 'Etc/GMT+0', + 'Etc/GMT+1', + 'Etc/GMT+10', + 'Etc/GMT+11', + 'Etc/GMT+12', + 'Etc/GMT+2', + 'Etc/GMT+3', + 'Etc/GMT+4', + 'Etc/GMT+5', + 'Etc/GMT+6', + 'Etc/GMT+7', + 'Etc/GMT+8', + 'Etc/GMT+9', + 'Etc/GMT0', + 'Etc/GMT-0', + 'Etc/GMT-1', + 'Etc/GMT-10', + 'Etc/GMT-11', + 'Etc/GMT-12', + 'Etc/GMT-13', + 'Etc/GMT-14', + 'Etc/GMT-2', + 'Etc/GMT-3', + 'Etc/GMT-4', + 'Etc/GMT-5', + 'Etc/GMT-6', + 'Etc/GMT-7', + 'Etc/GMT-8', + 'Etc/GMT-9', + 'Etc/Greenwich', + 'Etc/UCT', + 'Etc/Universal', + 'Etc/UTC', + 'Etc/Zulu', + 'Europe/Amsterdam', + 'Europe/Andorra', + 'Europe/Astrakhan', + 'Europe/Athens', + 'Europe/Belfast', + 'Europe/Belgrade', + 'Europe/Berlin', + 'Europe/Bratislava', + 'Europe/Brussels', + 'Europe/Bucharest', + 'Europe/Budapest', + 'Europe/Busingen', + 'Europe/Chisinau', + 'Europe/Copenhagen', + 'Europe/Dublin', + 'Europe/Gibraltar', + 'Europe/Guernsey', + 'Europe/Helsinki', + 'Europe/Isle_of_Man', + 'Europe/Istanbul', + 'Europe/Jersey', + 'Europe/Kaliningrad', + 'Europe/Kiev', + 'Europe/Kirov', + 'Europe/Lisbon', + 'Europe/Ljubljana', + 'Europe/London', + 'Europe/Luxembourg', + 'Europe/Madrid', + 'Europe/Malta', + 'Europe/Mariehamn', + 'Europe/Minsk', + 'Europe/Monaco', + 'Europe/Moscow', + 'Asia/Nicosia', + 'Europe/Oslo', + 'Europe/Paris', + 'Europe/Podgorica', + 'Europe/Prague', + 'Europe/Riga', + 'Europe/Rome', + 'Europe/Samara', + 'Europe/San_Marino', + 'Europe/Sarajevo', + 'Europe/Saratov', + 'Europe/Simferopol', + 'Europe/Skopje', + 'Europe/Sofia', + 'Europe/Stockholm', + 'Europe/Tallinn', + 'Europe/Tirane', + 'Europe/Tiraspol', + 'Europe/Ulyanovsk', + 'Europe/Uzhgorod', + 'Europe/Vaduz', + 'Europe/Vatican', + 'Europe/Vienna', + 'Europe/Vilnius', + 'Europe/Volgograd', + 'Europe/Warsaw', + 'Europe/Zagreb', + 'Europe/Zaporozhye', + 'Europe/Zurich', + 'GB', + 'GB-Eire', + 'GMT', + 'GMT+0', + 'GMT0', + 'GMT−0', + 'Greenwich', + 'Hongkong', + 'HST', + 'Iceland', + 'Indian/Antananarivo', + 'Indian/Chagos', + 'Indian/Christmas', + 'Indian/Cocos', + 'Indian/Comoro', + 'Indian/Kerguelen', + 'Indian/Mahe', + 'Indian/Maldives', + 'Indian/Mauritius', + 'Indian/Mayotte', + 'Indian/Reunion', + 'Iran', + 'Israel', + 'Jamaica', + 'Japan', + 'Kwajalein', + 'Libya', + 'MET', + 'Mexico/BajaNorte', + 'Mexico/BajaSur', + 'Mexico/General', + 'MST', + 'MST7MDT', + 'Navajo', + 'NZ', + 'NZ-CHAT', + 'Pacific/Apia', + 'Pacific/Auckland', + 'Pacific/Bougainville', + 'Pacific/Chatham', + 'Pacific/Chuuk', + 'Pacific/Easter', + 'Pacific/Efate', + 'Pacific/Enderbury', + 'Pacific/Fakaofo', + 'Pacific/Fiji', + 'Pacific/Funafuti', + 'Pacific/Galapagos', + 'Pacific/Gambier', + 'Pacific/Guadalcanal', + 'Pacific/Guam', + 'Pacific/Honolulu', + 'Pacific/Johnston', + 'Pacific/Kiritimati', + 'Pacific/Kosrae', + 'Pacific/Kwajalein', + 'Pacific/Majuro', + 'Pacific/Marquesas', + 'Pacific/Midway', + 'Pacific/Nauru', + 'Pacific/Niue', + 'Pacific/Norfolk', + 'Pacific/Noumea', + 'Pacific/Pago_Pago', + 'Pacific/Palau', + 'Pacific/Pitcairn', + 'Pacific/Pohnpei', + 'Pacific/Ponape', + 'Pacific/Port_Moresby', + 'Pacific/Rarotonga', + 'Pacific/Saipan', + 'Pacific/Samoa', + 'Pacific/Tahiti', + 'Pacific/Tarawa', + 'Pacific/Tongatapu', + 'Pacific/Truk', + 'Pacific/Wake', + 'Pacific/Wallis', + 'Pacific/Yap', + 'Poland', + 'Portugal', + 'PRC', + 'PST8PDT', + 'ROC', + 'ROK', + 'Singapore', + 'Turkey', + 'UCT', + 'Universal', + 'US/Alaska', + 'US/Aleutian', + 'US/Arizona', + 'US/Central', + 'US/Eastern', + 'US/East-Indiana', + 'US/Hawaii', + 'US/Indiana-Starke', + 'US/Michigan', + 'US/Mountain', + 'US/Pacific', + 'US/Pacific-New', + 'US/Samoa', + 'UTC', + 'WET', + 'W-SU', + 'Zulu' +] diff --git a/core/bootstrap/config.js b/core/bootstrap/config.js index 9f196c7..7a7818a 100644 --- a/core/bootstrap/config.js +++ b/core/bootstrap/config.js @@ -1,10 +1,10 @@ -module.exports = -{ - core: - { - locator: - { - 'core/bootstrap' : __dirname - } - } -} +module.exports = +{ + core: + { + locator: + { + 'core/bootstrap' : __dirname + } + } +} diff --git a/core/bootstrap/index.js b/core/bootstrap/index.js index 81e24ec..2c3620a 100644 --- a/core/bootstrap/index.js +++ b/core/bootstrap/index.js @@ -1,27 +1,27 @@ -class Bootstrap -{ - constructor(locator) - { - this.locator = locator - } - - async bootstrap() - { - const - configuration = this.locator.locate('core/configuration'), - bootstrapMap = configuration.find('core.bootstrap') - - for(const uid in bootstrapMap) - { - const serviceName = bootstrapMap[uid] - - if(serviceName) - { - const service = this.locator.locate(serviceName) - await service.bootstrap() - } - } - } -} - -module.exports = Bootstrap +class Bootstrap +{ + constructor(locator) + { + this.locator = locator + } + + async bootstrap() + { + const + configuration = this.locator.locate('core/configuration'), + bootstrapMap = configuration.find('core.bootstrap') + + for(const uid in bootstrapMap) + { + const serviceName = bootstrapMap[uid] + + if(serviceName) + { + const service = this.locator.locate(serviceName) + await service.bootstrap() + } + } + } +} + +module.exports = Bootstrap diff --git a/core/bootstrap/locator.js b/core/bootstrap/locator.js index 6fac8cd..36beb3c 100644 --- a/core/bootstrap/locator.js +++ b/core/bootstrap/locator.js @@ -1,16 +1,16 @@ -const Bootstrap = require('.') - -class BootstrapLocator -{ - constructor(locator) - { - this.locator = locator - } - - locate() - { - return new Bootstrap(this.locator) - } -} - -module.exports = BootstrapLocator +const Bootstrap = require('.') + +class BootstrapLocator +{ + constructor(locator) + { + this.locator = locator + } + + locate() + { + return new Bootstrap(this.locator) + } +} + +module.exports = BootstrapLocator diff --git a/core/cli/config.js b/core/cli/config.js index e9a617d..b219254 100644 --- a/core/cli/config.js +++ b/core/cli/config.js @@ -1,10 +1,10 @@ -module.exports = -{ - core: - { - locator: - { - 'core/cli' : __dirname - } - } -} +module.exports = +{ + core: + { + locator: + { + 'core/cli' : __dirname + } + } +} diff --git a/core/cli/index.js b/core/cli/index.js index 9c6c5c9..772c966 100644 --- a/core/cli/index.js +++ b/core/cli/index.js @@ -1,92 +1,92 @@ -const Debug = require('@superhero/debug') - -class Cli extends Debug -{ - /** - * @param {readline} readline - */ - constructor(readline) - { - const options = - { - date : false, - maxStringLength : 0, - separator : '\n' - } - super(options) - this.readline = readline - } - - /** - * @param {string} question - * @param {string} alternatives - * @see https://nodejs.org/api/readline.html#readline_use_of_the_completer_function - */ - question(question, alternatives) - { - return new Promise((accept, reject) => - { - const rl = this.readline.createInterface( - { - prompt : this.prompt, - input : process.stdin, - output : process.stdout, - completer : Array.isArray(alternatives) === false - ? undefined - : (line) => - { - const completions = alternatives.filter((c) => c.startsWith(line)) - return [ completions.length ? completions : alternatives, line ] - } - }) - - rl.question(question + ' ', async (input) => - { - rl.close() - - if(Array.isArray(alternatives) && alternatives.length) - { - if(alternatives.includes(input)) - { - accept(input) - } - else - { - if(alternatives.length === 1) - { - await this.write(`You must chose ${alternatives}`, 'red') - } - else if(alternatives.length < 10) - { - await this.write(`You must chose one of the alternatives ${alternatives.slice(0, -1).join(', ')} or ${alternatives.slice(-1)}`, 'red') - } - else - { - await this.write(`You must chose one of the ${alternatives.length} valid alternatives`, 'red') - await this.write(`You can use the "tab" key to list all the alternatives `, 'red') - } - - const answer = await this.question(question, alternatives) - accept(answer) - } - } - else - { - accept(input) - } - }) - }) - } - - /** - * @param {*} chunk - * @param {string} color - */ - write(chunk, color) - { - this.color(color).log(chunk) - this.color(false) - } -} - -module.exports = Cli +const Debug = require('@superhero/debug') + +class Cli extends Debug +{ + /** + * @param {readline} readline + */ + constructor(readline) + { + const options = + { + date : false, + maxStringLength : 0, + separator : '\n' + } + super(options) + this.readline = readline + } + + /** + * @param {string} question + * @param {string} alternatives + * @see https://nodejs.org/api/readline.html#readline_use_of_the_completer_function + */ + question(question, alternatives) + { + return new Promise((accept, reject) => + { + const rl = this.readline.createInterface( + { + prompt : this.prompt, + input : process.stdin, + output : process.stdout, + completer : Array.isArray(alternatives) === false + ? undefined + : (line) => + { + const completions = alternatives.filter((c) => c.startsWith(line)) + return [ completions.length ? completions : alternatives, line ] + } + }) + + rl.question(question + ' ', async (input) => + { + rl.close() + + if(Array.isArray(alternatives) && alternatives.length) + { + if(alternatives.includes(input)) + { + accept(input) + } + else + { + if(alternatives.length === 1) + { + await this.write(`You must chose ${alternatives}`, 'red') + } + else if(alternatives.length < 10) + { + await this.write(`You must chose one of the alternatives ${alternatives.slice(0, -1).join(', ')} or ${alternatives.slice(-1)}`, 'red') + } + else + { + await this.write(`You must chose one of the ${alternatives.length} valid alternatives`, 'red') + await this.write(`You can use the "tab" key to list all the alternatives `, 'red') + } + + const answer = await this.question(question, alternatives) + accept(answer) + } + } + else + { + accept(input) + } + }) + }) + } + + /** + * @param {*} chunk + * @param {string} color + */ + write(chunk, color) + { + this.color(color).log(chunk) + this.color(false) + } +} + +module.exports = Cli diff --git a/core/cli/locator.js b/core/cli/locator.js index 1a90e8f..b75a5e5 100644 --- a/core/cli/locator.js +++ b/core/cli/locator.js @@ -1,13 +1,13 @@ -const -Cli = require('.'), -readline = require('readline') - -class CliLocator -{ - locate() - { - return new Cli(readline) - } -} - -module.exports = CliLocator +const +Cli = require('.'), +readline = require('readline') + +class CliLocator +{ + locate() + { + return new Cli(readline) + } +} + +module.exports = CliLocator diff --git a/core/config.js b/core/config.js index 9883013..426e967 100644 --- a/core/config.js +++ b/core/config.js @@ -1,15 +1,15 @@ -module.exports = -{ - core: - { - eventbus: - { - observers: - { - 'core.error' : { 'core/console/observer/error' : true }, - 'core.warning' : { 'core/console/observer/warning' : true }, - 'core.info' : { 'core/console/observer/info' : true } - } - } - } -} +module.exports = +{ + core: + { + eventbus: + { + observers: + { + 'core.error' : { 'core/console/observer/error' : true }, + 'core.warning' : { 'core/console/observer/warning' : true }, + 'core.info' : { 'core/console/observer/info' : true } + } + } + } +} diff --git a/core/configuration/config.js b/core/configuration/config.js index 3d4a859..87eac28 100644 --- a/core/configuration/config.js +++ b/core/configuration/config.js @@ -1,10 +1,10 @@ -module.exports = -{ - core: - { - locator: - { - 'core/configuration' : __dirname - } - } -} +module.exports = +{ + core: + { + locator: + { + 'core/configuration' : __dirname + } + } +} diff --git a/core/configuration/index.js b/core/configuration/index.js index 18310e0..71ee6a1 100644 --- a/core/configuration/index.js +++ b/core/configuration/index.js @@ -1,29 +1,29 @@ -class Configuration -{ - constructor(deepclone, deepmerge, deepfind, deepfreeze) - { - this.deepclone = deepclone - this.deepmerge = deepmerge - this.deepfind = deepfind - this.deepfreeze = deepfreeze - this.config = {} - } - - extend(config) - { - const clone = this.deepclone.clone(config) - this.config = this.deepmerge.merge(this.config, clone) - } - - freeze() - { - this.deepfreeze.freeze(this.config) - } - - find(path) - { - return this.deepfind.find(path, this.config) - } -} - -module.exports = Configuration +class Configuration +{ + constructor(deepclone, deepmerge, deepfind, deepfreeze) + { + this.deepclone = deepclone + this.deepmerge = deepmerge + this.deepfind = deepfind + this.deepfreeze = deepfreeze + this.config = {} + } + + extend(config) + { + const clone = this.deepclone.clone(config) + this.config = this.deepmerge.merge(this.config, clone) + } + + freeze() + { + this.deepfreeze.freeze(this.config) + } + + find(path) + { + return this.deepfind.find(path, this.config) + } +} + +module.exports = Configuration diff --git a/core/configuration/locator.js b/core/configuration/locator.js index 4516456..a52cf9a 100644 --- a/core/configuration/locator.js +++ b/core/configuration/locator.js @@ -1,23 +1,23 @@ -const Configuration = require('.') - -class ConfigurationLocator -{ - constructor(locator) - { - this.locator = locator - } - - locate() - { - const - deepclone = this.locator.locate('core/deepclone'), - deepmerge = this.locator.locate('core/deepmerge'), - deepfind = this.locator.locate('core/deepfind'), - deepfreeze = this.locator.locate('core/deepfreeze'), - configuration = new Configuration(deepclone, deepmerge, deepfind, deepfreeze) - - return configuration - } -} - -module.exports = ConfigurationLocator +const Configuration = require('.') + +class ConfigurationLocator +{ + constructor(locator) + { + this.locator = locator + } + + locate() + { + const + deepclone = this.locator.locate('core/deepclone'), + deepmerge = this.locator.locate('core/deepmerge'), + deepfind = this.locator.locate('core/deepfind'), + deepfreeze = this.locator.locate('core/deepfreeze'), + configuration = new Configuration(deepclone, deepmerge, deepfind, deepfreeze) + + return configuration + } +} + +module.exports = ConfigurationLocator diff --git a/core/console/config.js b/core/console/config.js index c0ba4ef..c4382a5 100644 --- a/core/console/config.js +++ b/core/console/config.js @@ -1,18 +1,18 @@ -module.exports = -{ - core: - { - console: - { - color : 'cyan', - debug : true, - maxArrayLength : 2e2, - maxObjectDepth : 1e2, - maxStringLength : 1e4 - }, - locator: - { - 'core/console' : __dirname - } - } -} +module.exports = +{ + core: + { + console: + { + color : 'cyan', + debug : true, + maxArrayLength : 2e2, + maxObjectDepth : 1e2, + maxStringLength : 1e4 + }, + locator: + { + 'core/console' : __dirname + } + } +} diff --git a/core/console/index.js b/core/console/index.js index 94ae678..6c492a0 100644 --- a/core/console/index.js +++ b/core/console/index.js @@ -1,23 +1,23 @@ -const Debug = require('@superhero/debug') - -/** - * @extends {@superhero/debug} - */ -class Console extends Debug -{ - constructor(options) - { - super(options) - - const - error_options = { ...options, color:'red', prefix:'error:' }, - warning_options = { ...options, color:'yellow', prefix:'warning:' }, - error = new Debug(error_options), - warning = new Debug(warning_options) - - this.error = error.error.bind(error) - this.warning = warning.log.bind(warning) - } -} - -module.exports = Console +const Debug = require('@superhero/debug') + +/** + * @extends {@superhero/debug} + */ +class Console extends Debug +{ + constructor(options) + { + super(options) + + const + error_options = { ...options, color:'red', prefix:'error:' }, + warning_options = { ...options, color:'yellow', prefix:'warning:' }, + error = new Debug(error_options), + warning = new Debug(warning_options) + + this.error = error.error.bind(error) + this.warning = warning.log.bind(warning) + } +} + +module.exports = Console diff --git a/core/console/locator.js b/core/console/locator.js index 9e5eba8..22ad45f 100644 --- a/core/console/locator.js +++ b/core/console/locator.js @@ -1,21 +1,21 @@ -const Console = require('.') - -class ConsoleLocator -{ - constructor(locator) - { - this.locator = locator - } - - locate() - { - const - configuration = this.locator.locate('core/configuration'), - options = configuration.find('core.console'), - console = new Console(options) - - return console - } -} - -module.exports = ConsoleLocator +const Console = require('.') + +class ConsoleLocator +{ + constructor(locator) + { + this.locator = locator + } + + locate() + { + const + configuration = this.locator.locate('core/configuration'), + options = configuration.find('core.console'), + console = new Console(options) + + return console + } +} + +module.exports = ConsoleLocator diff --git a/core/console/observer/error/config.js b/core/console/observer/error/config.js index 1b3eb21..b6c0c98 100644 --- a/core/console/observer/error/config.js +++ b/core/console/observer/error/config.js @@ -1,10 +1,10 @@ -module.exports = -{ - core: - { - locator: - { - 'core/console/observer/error' : __dirname - } - } -} +module.exports = +{ + core: + { + locator: + { + 'core/console/observer/error' : __dirname + } + } +} diff --git a/core/console/observer/error/index.js b/core/console/observer/error/index.js index 42f845d..68cf4fc 100644 --- a/core/console/observer/error/index.js +++ b/core/console/observer/error/index.js @@ -1,14 +1,14 @@ -class ConsoleObserverError -{ - constructor(console) - { - this.console = console - } - - observe(event) - { - this.console.error(event) - } -} - -module.exports = ConsoleObserverError +class ConsoleObserverError +{ + constructor(console) + { + this.console = console + } + + observe(event) + { + this.console.error(event) + } +} + +module.exports = ConsoleObserverError diff --git a/core/console/observer/error/locator.js b/core/console/observer/error/locator.js index 28aac99..c7ab81e 100644 --- a/core/console/observer/error/locator.js +++ b/core/console/observer/error/locator.js @@ -1,17 +1,17 @@ -const ConsoleObserverError = require('.') - -class ConsoleObserverErrorLocator -{ - constructor(locator) - { - this.locator = locator - } - - locate() - { - const console = this.locator.locate('core/console') - return new ConsoleObserverError(console) - } -} - -module.exports = ConsoleObserverErrorLocator +const ConsoleObserverError = require('.') + +class ConsoleObserverErrorLocator +{ + constructor(locator) + { + this.locator = locator + } + + locate() + { + const console = this.locator.locate('core/console') + return new ConsoleObserverError(console) + } +} + +module.exports = ConsoleObserverErrorLocator diff --git a/core/console/observer/info/config.js b/core/console/observer/info/config.js index 96db217..5fa736b 100644 --- a/core/console/observer/info/config.js +++ b/core/console/observer/info/config.js @@ -1,10 +1,10 @@ -module.exports = -{ - core: - { - locator: - { - 'core/console/observer/info' : __dirname - } - } -} +module.exports = +{ + core: + { + locator: + { + 'core/console/observer/info' : __dirname + } + } +} diff --git a/core/console/observer/info/index.js b/core/console/observer/info/index.js index 92b8331..d6574fd 100644 --- a/core/console/observer/info/index.js +++ b/core/console/observer/info/index.js @@ -1,14 +1,14 @@ -class ConsoleObserverInfo -{ - constructor(console) - { - this.console = console - } - - observe(event) - { - this.console.info(event) - } -} - -module.exports = ConsoleObserverInfo +class ConsoleObserverInfo +{ + constructor(console) + { + this.console = console + } + + observe(event) + { + this.console.info(event) + } +} + +module.exports = ConsoleObserverInfo diff --git a/core/console/observer/info/locator.js b/core/console/observer/info/locator.js index a3eccc1..6555670 100644 --- a/core/console/observer/info/locator.js +++ b/core/console/observer/info/locator.js @@ -1,17 +1,17 @@ -const ConsoleObserverInfo = require('.') - -class ConsoleObserverInfoLocator -{ - constructor(locator) - { - this.locator = locator - } - - locate() - { - const console = this.locator.locate('core/console') - return new ConsoleObserverInfo(console) - } -} - -module.exports = ConsoleObserverInfoLocator +const ConsoleObserverInfo = require('.') + +class ConsoleObserverInfoLocator +{ + constructor(locator) + { + this.locator = locator + } + + locate() + { + const console = this.locator.locate('core/console') + return new ConsoleObserverInfo(console) + } +} + +module.exports = ConsoleObserverInfoLocator diff --git a/core/console/observer/warning/config.js b/core/console/observer/warning/config.js index 170ce8f..f098c6d 100644 --- a/core/console/observer/warning/config.js +++ b/core/console/observer/warning/config.js @@ -1,10 +1,10 @@ -module.exports = -{ - core: - { - locator: - { - 'core/console/observer/warning' : __dirname - } - } -} +module.exports = +{ + core: + { + locator: + { + 'core/console/observer/warning' : __dirname + } + } +} diff --git a/core/console/observer/warning/index.js b/core/console/observer/warning/index.js index f157998..2506ef8 100644 --- a/core/console/observer/warning/index.js +++ b/core/console/observer/warning/index.js @@ -1,14 +1,14 @@ -class ConsoleObserverWarning -{ - constructor(console) - { - this.console = console - } - - observe(event) - { - this.console.warning(event) - } -} - -module.exports = ConsoleObserverWarning +class ConsoleObserverWarning +{ + constructor(console) + { + this.console = console + } + + observe(event) + { + this.console.warning(event) + } +} + +module.exports = ConsoleObserverWarning diff --git a/core/console/observer/warning/locator.js b/core/console/observer/warning/locator.js index 5fec9b2..15f7878 100644 --- a/core/console/observer/warning/locator.js +++ b/core/console/observer/warning/locator.js @@ -1,17 +1,17 @@ -const ConsoleObserverWarning = require('.') - -class ConsoleObserverWarningLocator -{ - constructor(locator) - { - this.locator = locator - } - - locate() - { - const console = this.locator.locate('core/console') - return new ConsoleObserverWarning(console) - } -} - -module.exports = ConsoleObserverWarningLocator +const ConsoleObserverWarning = require('.') + +class ConsoleObserverWarningLocator +{ + constructor(locator) + { + this.locator = locator + } + + locate() + { + const console = this.locator.locate('core/console') + return new ConsoleObserverWarning(console) + } +} + +module.exports = ConsoleObserverWarningLocator diff --git a/core/deepclone/config.js b/core/deepclone/config.js index 37e58a0..d19bb61 100644 --- a/core/deepclone/config.js +++ b/core/deepclone/config.js @@ -1,10 +1,10 @@ -module.exports = -{ - core: - { - locator: - { - 'core/deepclone' : __dirname - } - } -} +module.exports = +{ + core: + { + locator: + { + 'core/deepclone' : __dirname + } + } +} diff --git a/core/deepclone/error/failed-to-clone.js b/core/deepclone/error/failed-to-clone.js index 359ab5f..b1b3af8 100644 --- a/core/deepclone/error/failed-to-clone.js +++ b/core/deepclone/error/failed-to-clone.js @@ -1,10 +1,10 @@ -class FailedToCloneError extends Error -{ - constructor(...args) - { - super(...args) - this.code = 'E_FAILED_TO_CLONE' - } -} - -module.exports = FailedToCloneError +class FailedToCloneError extends Error +{ + constructor(...args) + { + super(...args) + this.code = 'E_FAILED_TO_CLONE' + } +} + +module.exports = FailedToCloneError diff --git a/core/deepclone/index.js b/core/deepclone/index.js index efc378a..139e049 100644 --- a/core/deepclone/index.js +++ b/core/deepclone/index.js @@ -1,18 +1,18 @@ -const FailedToCloneError = require('./error/failed-to-clone') - -class DeepClone -{ - clone(obj) - { - try - { - return JSON.parse(JSON.stringify(obj)) - } - catch(error) - { - throw new FailedToCloneError(error.message) - } - } -} - -module.exports = DeepClone +const FailedToCloneError = require('./error/failed-to-clone') + +class DeepClone +{ + clone(obj) + { + try + { + return JSON.parse(JSON.stringify(obj)) + } + catch(error) + { + throw new FailedToCloneError(error.message) + } + } +} + +module.exports = DeepClone diff --git a/core/deepclone/locator.js b/core/deepclone/locator.js index 052ee3e..4db3cba 100644 --- a/core/deepclone/locator.js +++ b/core/deepclone/locator.js @@ -1,11 +1,11 @@ -const DeepClone = require('.') - -class DeepCloneLocator -{ - locate() - { - return new DeepClone - } -} - -module.exports = DeepCloneLocator +const DeepClone = require('.') + +class DeepCloneLocator +{ + locate() + { + return new DeepClone + } +} + +module.exports = DeepCloneLocator diff --git a/core/deepfind/config.js b/core/deepfind/config.js index c7c5316..cd5ae7f 100644 --- a/core/deepfind/config.js +++ b/core/deepfind/config.js @@ -1,10 +1,10 @@ -module.exports = -{ - core: - { - locator: - { - 'core/deepfind' : __dirname - } - } -} +module.exports = +{ + core: + { + locator: + { + 'core/deepfind' : __dirname + } + } +} diff --git a/core/deepfind/index.js b/core/deepfind/index.js index 37ddadc..b406706 100644 --- a/core/deepfind/index.js +++ b/core/deepfind/index.js @@ -1,11 +1,11 @@ -class DeepFind -{ - find(path, obj) - { - // split on "." or "/" - const keys = path.split(/\.|\//) - return keys.reduce((obj, key) => obj && obj[key], obj) - } -} - -module.exports = DeepFind +class DeepFind +{ + find(path, obj) + { + // split on "." or "/" + const keys = path.split(/\.|\//) + return keys.reduce((obj, key) => obj && obj[key], obj) + } +} + +module.exports = DeepFind diff --git a/core/deepfind/locator.js b/core/deepfind/locator.js index 1318304..cfd6f1a 100644 --- a/core/deepfind/locator.js +++ b/core/deepfind/locator.js @@ -1,11 +1,11 @@ -const DeepFind = require('.') - -class DeepFindLocator -{ - locate() - { - return new DeepFind - } -} - -module.exports = DeepFindLocator +const DeepFind = require('.') + +class DeepFindLocator +{ + locate() + { + return new DeepFind + } +} + +module.exports = DeepFindLocator diff --git a/core/deepfreeze/config.js b/core/deepfreeze/config.js index 4fcefa2..b063094 100644 --- a/core/deepfreeze/config.js +++ b/core/deepfreeze/config.js @@ -1,10 +1,10 @@ -module.exports = -{ - core: - { - locator: - { - 'core/deepfreeze' : __dirname - } - } -} +module.exports = +{ + core: + { + locator: + { + 'core/deepfreeze' : __dirname + } + } +} diff --git a/core/deepfreeze/index.js b/core/deepfreeze/index.js index aff72b2..a0f7c6c 100644 --- a/core/deepfreeze/index.js +++ b/core/deepfreeze/index.js @@ -1,20 +1,20 @@ -class DeepFreeze -{ - freeze(obj) - { - const propNames = Object.getOwnPropertyNames(obj) - - for (const name of propNames) - { - const value = obj[name] - - obj[name] = value && typeof value === 'object' - ? this.freeze(value) - : value - } - - return Object.freeze(obj) - } -} - -module.exports = DeepFreeze +class DeepFreeze +{ + freeze(obj) + { + const propNames = Object.getOwnPropertyNames(obj) + + for (const name of propNames) + { + const value = obj[name] + + obj[name] = value && typeof value === 'object' + ? this.freeze(value) + : value + } + + return Object.freeze(obj) + } +} + +module.exports = DeepFreeze diff --git a/core/deepfreeze/locator.js b/core/deepfreeze/locator.js index a0010a0..e6ee3a6 100644 --- a/core/deepfreeze/locator.js +++ b/core/deepfreeze/locator.js @@ -1,11 +1,11 @@ -const DeepFreeze = require('.') - -class DeepFreezeLocator -{ - locate() - { - return new DeepFreeze - } -} - -module.exports = DeepFreezeLocator +const DeepFreeze = require('.') + +class DeepFreezeLocator +{ + locate() + { + return new DeepFreeze + } +} + +module.exports = DeepFreezeLocator diff --git a/core/deepmerge/config.js b/core/deepmerge/config.js index 716982d..3dd4a1b 100644 --- a/core/deepmerge/config.js +++ b/core/deepmerge/config.js @@ -1,10 +1,10 @@ -module.exports = -{ - core: - { - locator: - { - 'core/deepmerge' : __dirname - } - } -} +module.exports = +{ + core: + { + locator: + { + 'core/deepmerge' : __dirname + } + } +} diff --git a/core/deepmerge/index.js b/core/deepmerge/index.js index 7136c9c..ffbc5be 100644 --- a/core/deepmerge/index.js +++ b/core/deepmerge/index.js @@ -1,42 +1,42 @@ -class DeepMerge -{ - merge(a, b, ...c) - { - const result = this._merge(a, b) - - return c.length - ? this.merge(result, c[0], ...c.slice(1)) - : result - } - - _merge(a, b) - { - if(typeof a !== 'object' || a === null) - return b - - return Array.isArray(a) - ? this._mergeArray (a, b) - : this._mergeObject(a, b) - } - - _mergeArray(a, b) - { - if(!Array.isArray(b)) - return b - - a.push(...b) - return a - } - - _mergeObject(a, b) - { - for(const key in b) - a[key] = key in a - ? this._merge(a[key], b[key]) - : b[key] - - return a - } -} - -module.exports = DeepMerge +class DeepMerge +{ + merge(a, b, ...c) + { + const result = this._merge(a, b) + + return c.length + ? this.merge(result, c[0], ...c.slice(1)) + : result + } + + _merge(a, b) + { + if(typeof a !== 'object' || a === null) + return b + + return Array.isArray(a) + ? this._mergeArray (a, b) + : this._mergeObject(a, b) + } + + _mergeArray(a, b) + { + if(!Array.isArray(b)) + return b + + a.push(...b) + return a + } + + _mergeObject(a, b) + { + for(const key in b) + a[key] = key in a + ? this._merge(a[key], b[key]) + : b[key] + + return a + } +} + +module.exports = DeepMerge diff --git a/core/deepmerge/locator.js b/core/deepmerge/locator.js index 948f3fc..f2945df 100644 --- a/core/deepmerge/locator.js +++ b/core/deepmerge/locator.js @@ -1,11 +1,11 @@ -const DeepMerge = require('.') - -class DeepMergeLocator -{ - locate() - { - return new DeepMerge - } -} - -module.exports = DeepMergeLocator +const DeepMerge = require('.') + +class DeepMergeLocator +{ + locate() + { + return new DeepMerge + } +} + +module.exports = DeepMergeLocator diff --git a/core/eventbus/bootstrap/error/observer-contract-not-honered.js b/core/eventbus/bootstrap/error/observer-contract-not-honered.js index a9bce97..1e9f563 100644 --- a/core/eventbus/bootstrap/error/observer-contract-not-honered.js +++ b/core/eventbus/bootstrap/error/observer-contract-not-honered.js @@ -1,10 +1,10 @@ -class ObserverContractNotHoneredError extends Error -{ - constructor(...a) - { - super(...a) - this.code = 'E_EVENTBUS_OBSERVER_CONTRACT_NOT_HONERED' - } -} - -module.exports = ObserverContractNotHoneredError +class ObserverContractNotHoneredError extends Error +{ + constructor(...a) + { + super(...a) + this.code = 'E_EVENTBUS_OBSERVER_CONTRACT_NOT_HONERED' + } +} + +module.exports = ObserverContractNotHoneredError diff --git a/core/eventbus/bootstrap/index.js b/core/eventbus/bootstrap/index.js index 1061fd7..0dd95f8 100644 --- a/core/eventbus/bootstrap/index.js +++ b/core/eventbus/bootstrap/index.js @@ -1,39 +1,39 @@ -const ObserverContractNotHoneredError = require('./error/observer-contract-not-honered') - -class EventbusBootstrap -{ - constructor(configuration, eventbus, locator) - { - this.configuration = configuration - this.eventbus = eventbus - this.locator = locator - } - - bootstrap() - { - const observers = this.configuration.find('core.eventbus.observers') - - for(const event in observers) - { - for(const serviceName in observers[event]) - { - if(!observers[event][serviceName]) - { - continue - } - - const service = this.locator.locate(serviceName) - - if(typeof service.observe !== 'function') - { - const msg = `"${serviceName}" does not implement the EventBusObserver interface` - throw new ObserverContractNotHoneredError(msg) - } - - this.eventbus.on(event, (data) => service.observe(data, event)) - } - } - } -} - -module.exports = EventbusBootstrap +const ObserverContractNotHoneredError = require('./error/observer-contract-not-honered') + +class EventbusBootstrap +{ + constructor(configuration, eventbus, locator) + { + this.configuration = configuration + this.eventbus = eventbus + this.locator = locator + } + + bootstrap() + { + const observers = this.configuration.find('core.eventbus.observers') + + for(const event in observers) + { + for(const serviceName in observers[event]) + { + if(!observers[event][serviceName]) + { + continue + } + + const service = this.locator.locate(serviceName) + + if(typeof service.observe !== 'function') + { + const msg = `"${serviceName}" does not implement the EventBusObserver interface` + throw new ObserverContractNotHoneredError(msg) + } + + this.eventbus.on(event, (data) => service.observe(data, event)) + } + } + } +} + +module.exports = EventbusBootstrap diff --git a/core/eventbus/bootstrap/locator.js b/core/eventbus/bootstrap/locator.js index 1e96659..3ec4bb9 100644 --- a/core/eventbus/bootstrap/locator.js +++ b/core/eventbus/bootstrap/locator.js @@ -1,20 +1,20 @@ -const EventbusBootstrap = require('.') - -class EventbusBootstrapLocator -{ - constructor(locator) - { - this.locator = locator - } - - locate() - { - const - configuration = this.locator.locate('core/configuration'), - eventbus = this.locator.locate('core/eventbus') - - return new EventbusBootstrap(configuration, eventbus, this.locator) - } -} - -module.exports = EventbusBootstrapLocator +const EventbusBootstrap = require('.') + +class EventbusBootstrapLocator +{ + constructor(locator) + { + this.locator = locator + } + + locate() + { + const + configuration = this.locator.locate('core/configuration'), + eventbus = this.locator.locate('core/eventbus') + + return new EventbusBootstrap(configuration, eventbus, this.locator) + } +} + +module.exports = EventbusBootstrapLocator diff --git a/core/eventbus/config.js b/core/eventbus/config.js index 0f7c660..77ec83a 100644 --- a/core/eventbus/config.js +++ b/core/eventbus/config.js @@ -1,20 +1,20 @@ -module.exports = -{ - core: - { - bootstrap: - { - 'eventbus' : 'core/eventbus/bootstrap' - }, - locator: - { - 'core/eventbus' : __dirname, - 'core/eventbus/bootstrap' : __dirname + '/bootstrap' - }, - eventbus: - { - options : {}, - observers : {} - } - } -} +module.exports = +{ + core: + { + bootstrap: + { + 'eventbus' : 'core/eventbus/bootstrap' + }, + locator: + { + 'core/eventbus' : __dirname, + 'core/eventbus/bootstrap' : __dirname + '/bootstrap' + }, + eventbus: + { + options : {}, + observers : {} + } + } +} diff --git a/core/eventbus/index.js b/core/eventbus/index.js index eadb014..d87dfed 100644 --- a/core/eventbus/index.js +++ b/core/eventbus/index.js @@ -1,26 +1,26 @@ -const Events = require('events') - -class Eventbus extends Events -{ - constructor(options, console) - { - super(options) - - this.warnings = [] - this.console = console - } - - emit(name, data) - { - if(!this.listenerCount(name) - && !this.warnings.includes(name)) - { - this.warnings.push(name) - this.console.warning(`event: "${name}" does not have a defined observer`) - } - - super.emit(name, data) - } -} - -module.exports = Eventbus +const Events = require('events') + +class Eventbus extends Events +{ + constructor(options, console) + { + super(options) + + this.warnings = [] + this.console = console + } + + emit(name, data) + { + if(!this.listenerCount(name) + && !this.warnings.includes(name)) + { + this.warnings.push(name) + this.console.warning(`event: "${name}" does not have a defined observer`) + } + + super.emit(name, data) + } +} + +module.exports = Eventbus diff --git a/core/eventbus/locator.js b/core/eventbus/locator.js index 5645001..576f9ee 100644 --- a/core/eventbus/locator.js +++ b/core/eventbus/locator.js @@ -1,22 +1,22 @@ -const Eventbus = require('.') - -class EventbusLocator -{ - constructor(locator) - { - this.locator = locator - } - - locate() - { - const - configuration = this.locator.locate('core/configuration'), - eventbusOptions = configuration.find('core.eventbus.options'), - console = this.locator.locate('core/console'), - eventbus = new Eventbus(eventbusOptions, console) - - return eventbus - } -} - -module.exports = EventbusLocator +const Eventbus = require('.') + +class EventbusLocator +{ + constructor(locator) + { + this.locator = locator + } + + locate() + { + const + configuration = this.locator.locate('core/configuration'), + eventbusOptions = configuration.find('core.eventbus.options'), + console = this.locator.locate('core/console'), + eventbus = new Eventbus(eventbusOptions, console) + + return eventbus + } +} + +module.exports = EventbusLocator diff --git a/core/eventbus/observer.js b/core/eventbus/observer.js index 4973f52..d87bc1a 100644 --- a/core/eventbus/observer.js +++ b/core/eventbus/observer.js @@ -1,10 +1,10 @@ -/** - * @interface EventbusObserver - */ - -/** - * @function EventbusObserver#observe - * @param {*} data - * @param {string} eventName - * @returns {void} - */ +/** + * @interface EventbusObserver + */ + +/** + * @function EventbusObserver#observe + * @param {*} data + * @param {string} eventName + * @returns {void} + */ diff --git a/core/factory.js b/core/factory.js index 278849e..d88a0f9 100644 --- a/core/factory.js +++ b/core/factory.js @@ -1,59 +1,59 @@ -const -Core = require('..'), -Configuration = require('./configuration'), -Deepclone = require('./deepclone'), -Deepfind = require('./deepfind'), -Deepfreeze = require('./deepfreeze'), -Deepmerge = require('./deepmerge'), -Locator = require('./locator'), -Path = require('./path') - -class CoreFactory -{ - create() - { - const - locator = this.createLocator(), - core = new Core(locator) - - core.add('core/bootstrap') - core.add('core/cli') - core.add('core/console') - core.add('core/console/observer/error') - core.add('core/console/observer/info') - core.add('core/console/observer/warning') - core.add('core/eventbus') - core.add('core/http/request') - core.add('core/http/server') - core.add('core/object') - core.add('core/process') - core.add('core/schema') - core.add('core/string') - core.add('core') - - return core - } - - createLocator() - { - const - locator = new Locator, - deepclone = new Deepclone, - deepfreeze = new Deepfreeze, - deepmerge = new Deepmerge, - deepfind = new Deepfind, - path = new Path, - configuration = new Configuration(deepclone, deepmerge, deepfind, deepfreeze) - - locator.set('core/deepclone', deepclone) - locator.set('core/deepfind', deepfind) - locator.set('core/deepfreeze', deepfreeze) - locator.set('core/deepmerge', deepmerge) - locator.set('core/path', path) - locator.set('core/configuration', configuration) - - return locator - } -} - -module.exports = CoreFactory +const +Core = require('..'), +Configuration = require('./configuration'), +Deepclone = require('./deepclone'), +Deepfind = require('./deepfind'), +Deepfreeze = require('./deepfreeze'), +Deepmerge = require('./deepmerge'), +Locator = require('./locator'), +Path = require('./path') + +class CoreFactory +{ + create() + { + const + locator = this.createLocator(), + core = new Core(locator) + + core.add('core/bootstrap') + core.add('core/cli') + core.add('core/console') + core.add('core/console/observer/error') + core.add('core/console/observer/info') + core.add('core/console/observer/warning') + core.add('core/eventbus') + core.add('core/http/request') + core.add('core/http/server') + core.add('core/object') + core.add('core/process') + core.add('core/schema') + core.add('core/string') + core.add('core') + + return core + } + + createLocator() + { + const + locator = new Locator, + deepclone = new Deepclone, + deepfreeze = new Deepfreeze, + deepmerge = new Deepmerge, + deepfind = new Deepfind, + path = new Path, + configuration = new Configuration(deepclone, deepmerge, deepfind, deepfreeze) + + locator.set('core/deepclone', deepclone) + locator.set('core/deepfind', deepfind) + locator.set('core/deepfreeze', deepfreeze) + locator.set('core/deepmerge', deepmerge) + locator.set('core/path', path) + locator.set('core/configuration', configuration) + + return locator + } +} + +module.exports = CoreFactory diff --git a/core/http/request/config.js b/core/http/request/config.js index c9a16f1..9513e5d 100644 --- a/core/http/request/config.js +++ b/core/http/request/config.js @@ -1,10 +1,10 @@ -module.exports = -{ - core: - { - locator: - { - 'core/http/request' : __dirname - } - } -} +module.exports = +{ + core: + { + locator: + { + 'core/http/request' : __dirname + } + } +} diff --git a/core/http/request/index.js b/core/http/request/index.js index 3df35b8..32d3489 100644 --- a/core/http/request/index.js +++ b/core/http/request/index.js @@ -1,26 +1,26 @@ -const Request = require('@superhero/request') - -class HttpRequest extends Request -{ - constructor(options, object) - { - super(options) - this.object = object - } - - fetch(method, options) - { - if(typeof options === 'object') - { - options.headers = this.object.composeLowerCaseKeyedObject(options.headers) - } - - const response = super.fetch(method, options) - - response.headers = this.object.composeLowerCaseKeyedObject(response.headers) - - return response - } -} - -module.exports = HttpRequest +const Request = require('@superhero/request') + +class HttpRequest extends Request +{ + constructor(options, object) + { + super(options) + this.object = object + } + + fetch(method, options) + { + if(typeof options === 'object') + { + options.headers = this.object.composeLowerCaseKeyedObject(options.headers) + } + + const response = super.fetch(method, options) + + response.headers = this.object.composeLowerCaseKeyedObject(response.headers) + + return response + } +} + +module.exports = HttpRequest diff --git a/core/http/request/locator.js b/core/http/request/locator.js index 3ec91ba..21036bb 100644 --- a/core/http/request/locator.js +++ b/core/http/request/locator.js @@ -1,22 +1,22 @@ -const HttpRequest = require('.') - -class HttpRequestLocator -{ - constructor(locator) - { - this.locator = locator - } - - locate() - { - const - configuration = this.locator.locate('core/configuration'), - options = configuration.find('core.http.request.options'), - object = this.locator.locate('core/object'), - httpRequest = new HttpRequest(options, object) - - return httpRequest - } -} - -module.exports = HttpRequestLocator +const HttpRequest = require('.') + +class HttpRequestLocator +{ + constructor(locator) + { + this.locator = locator + } + + locate() + { + const + configuration = this.locator.locate('core/configuration'), + options = configuration.find('core.http.request.options'), + object = this.locator.locate('core/object'), + httpRequest = new HttpRequest(options, object) + + return httpRequest + } +} + +module.exports = HttpRequestLocator diff --git a/core/http/server/config.js b/core/http/server/config.js index d478ab0..51d491a 100644 --- a/core/http/server/config.js +++ b/core/http/server/config.js @@ -1,49 +1,49 @@ -module.exports = -{ - core: - { - bootstrap: - { - 'http/server/route/builder' : 'core/http/server/route/builder/bootstrap' - }, - locator: - { - 'core/http/server/dispatcher/chain' : __dirname + '/dispatcher/chain', - 'core/http/server/dispatcher/collection/builder' : __dirname + '/dispatcher/collection/builder', - 'core/http/server/request/builder' : __dirname + '/request/builder', - 'core/http/server/route/builder/bootstrap' : __dirname + '/route/builder/bootstrap', - 'core/http/server/route/builder/dto/builder/request-body' : __dirname + '/route/builder/dto/builder/request-body', - 'core/http/server/route/builder/dto/builder/request-query' : __dirname + '/route/builder/dto/builder/request-query', - 'core/http/server/route/builder/dto/builder/request-url' : __dirname + '/route/builder/dto/builder/request-url', - 'core/http/server/route/builder' : __dirname + '/route/builder', - 'core/http/server/session/builder' : __dirname + '/session/builder', - 'core/http/server/view/json' : __dirname + '/view/json', - 'core/http/server/view/stream' : __dirname + '/view/stream', - 'core/http/server/view/text' : __dirname + '/view/text', - 'core/http/server/view' : __dirname + '/view', - 'core/http/server' : __dirname - }, - http: - { - server: - { - timeout: 30e3, - route: - { - builder: - { - dto: - { - builders: - { - 'request-body' : 'core/http/server/route/builder/dto/builder/request-body', - 'request-query' : 'core/http/server/route/builder/dto/builder/request-query', - 'request-url' : 'core/http/server/route/builder/dto/builder/request-url' - } - } - } - } - } - } - } -} +module.exports = +{ + core: + { + bootstrap: + { + 'http/server/route/builder' : 'core/http/server/route/builder/bootstrap' + }, + locator: + { + 'core/http/server/dispatcher/chain' : __dirname + '/dispatcher/chain', + 'core/http/server/dispatcher/collection/builder' : __dirname + '/dispatcher/collection/builder', + 'core/http/server/request/builder' : __dirname + '/request/builder', + 'core/http/server/route/builder/bootstrap' : __dirname + '/route/builder/bootstrap', + 'core/http/server/route/builder/dto/builder/request-body' : __dirname + '/route/builder/dto/builder/request-body', + 'core/http/server/route/builder/dto/builder/request-query' : __dirname + '/route/builder/dto/builder/request-query', + 'core/http/server/route/builder/dto/builder/request-url' : __dirname + '/route/builder/dto/builder/request-url', + 'core/http/server/route/builder' : __dirname + '/route/builder', + 'core/http/server/session/builder' : __dirname + '/session/builder', + 'core/http/server/view/json' : __dirname + '/view/json', + 'core/http/server/view/stream' : __dirname + '/view/stream', + 'core/http/server/view/text' : __dirname + '/view/text', + 'core/http/server/view' : __dirname + '/view', + 'core/http/server' : __dirname + }, + http: + { + server: + { + timeout: 30e3, + route: + { + builder: + { + dto: + { + builders: + { + 'request-body' : 'core/http/server/route/builder/dto/builder/request-body', + 'request-query' : 'core/http/server/route/builder/dto/builder/request-query', + 'request-url' : 'core/http/server/route/builder/dto/builder/request-url' + } + } + } + } + } + } + } +} diff --git a/core/http/server/dispatcher/chain/error/dispatcher-chain-ended.js b/core/http/server/dispatcher/chain/error/dispatcher-chain-ended.js index c0abcf7..f3c6786 100644 --- a/core/http/server/dispatcher/chain/error/dispatcher-chain-ended.js +++ b/core/http/server/dispatcher/chain/error/dispatcher-chain-ended.js @@ -1,10 +1,10 @@ -class DispatcherChainEndedError extends Error -{ - constructor(...args) - { - super(...args) - this.code = 'E_DISPATCHER_CHAIN_ENDED' - } -} - -module.exports = DispatcherChainEndedError +class DispatcherChainEndedError extends Error +{ + constructor(...args) + { + super(...args) + this.code = 'E_DISPATCHER_CHAIN_ENDED' + } +} + +module.exports = DispatcherChainEndedError diff --git a/core/http/server/dispatcher/chain/index.js b/core/http/server/dispatcher/chain/index.js index 92285dd..35a24e6 100644 --- a/core/http/server/dispatcher/chain/index.js +++ b/core/http/server/dispatcher/chain/index.js @@ -1,40 +1,40 @@ -const DispatcherChainEndedError = require('./error/dispatcher-chain-ended') - -class ServerDispatcherChain -{ - constructor(path) - { - this.path = path - } - - async chain(dispatchers, i) - { - const - dispatcher = dispatchers[i++], - next = this.dispatch.bind(this, dispatchers, i) - - try - { - await dispatcher.dispatch(next) - } - catch(error) - { - await dispatcher.onError(error) - } - } - - async dispatch(dispatchers, i = 0) - { - if(i < dispatchers.length) - { - await this.chain(dispatchers, i) - } - else - { - const msg = `dispatcher chain has already finished "${i}/${dispatchers.length}"` - throw new DispatcherChainEndedError(msg) - } - } -} - -module.exports = ServerDispatcherChain +const DispatcherChainEndedError = require('./error/dispatcher-chain-ended') + +class ServerDispatcherChain +{ + constructor(path) + { + this.path = path + } + + async chain(dispatchers, i) + { + const + dispatcher = dispatchers[i++], + next = this.dispatch.bind(this, dispatchers, i) + + try + { + await dispatcher.dispatch(next) + } + catch(error) + { + await dispatcher.onError(error) + } + } + + async dispatch(dispatchers, i = 0) + { + if(i < dispatchers.length) + { + await this.chain(dispatchers, i) + } + else + { + const msg = `dispatcher chain has already finished "${i}/${dispatchers.length}"` + throw new DispatcherChainEndedError(msg) + } + } +} + +module.exports = ServerDispatcherChain diff --git a/core/http/server/dispatcher/chain/locator.js b/core/http/server/dispatcher/chain/locator.js index 9cc84b9..2dc3e5b 100644 --- a/core/http/server/dispatcher/chain/locator.js +++ b/core/http/server/dispatcher/chain/locator.js @@ -1,20 +1,20 @@ -const ServerDispatcherChain = require('.') - -class ServerDispatcherChainLocator -{ - constructor(locator) - { - this.locator = locator - } - - locate() - { - const - path = this.locator.locate('core/path'), - dispatcherChain = new ServerDispatcherChain(path) - - return dispatcherChain - } -} - -module.exports = ServerDispatcherChainLocator +const ServerDispatcherChain = require('.') + +class ServerDispatcherChainLocator +{ + constructor(locator) + { + this.locator = locator + } + + locate() + { + const + path = this.locator.locate('core/path'), + dispatcherChain = new ServerDispatcherChain(path) + + return dispatcherChain + } +} + +module.exports = ServerDispatcherChainLocator diff --git a/core/http/server/dispatcher/collection/builder/error/dispatcher-can-not-be-resolved.js b/core/http/server/dispatcher/collection/builder/error/dispatcher-can-not-be-resolved.js index 82b31dc..16df195 100644 --- a/core/http/server/dispatcher/collection/builder/error/dispatcher-can-not-be-resolved.js +++ b/core/http/server/dispatcher/collection/builder/error/dispatcher-can-not-be-resolved.js @@ -1,10 +1,10 @@ -class DispatcherCanNotBeResolvedError extends ReferenceError -{ - constructor(...args) - { - super(...args) - this.code = 'E_DISPATCHER_CAN_NOT_BE_RESOLVED' - } -} - -module.exports = DispatcherCanNotBeResolvedError +class DispatcherCanNotBeResolvedError extends ReferenceError +{ + constructor(...args) + { + super(...args) + this.code = 'E_DISPATCHER_CAN_NOT_BE_RESOLVED' + } +} + +module.exports = DispatcherCanNotBeResolvedError diff --git a/core/http/server/dispatcher/collection/builder/error/not-honering-dispatcher-contract.js b/core/http/server/dispatcher/collection/builder/error/not-honering-dispatcher-contract.js index be4de3a..3dbb620 100644 --- a/core/http/server/dispatcher/collection/builder/error/not-honering-dispatcher-contract.js +++ b/core/http/server/dispatcher/collection/builder/error/not-honering-dispatcher-contract.js @@ -1,10 +1,10 @@ -class NotHoneringDispatcherContractError extends Error -{ - constructor(...args) - { - super(...args) - this.code = 'E_NOT_HONERING_DISPATCHER_CONTRACT' - } -} - -module.exports = NotHoneringDispatcherContractError +class NotHoneringDispatcherContractError extends Error +{ + constructor(...args) + { + super(...args) + this.code = 'E_NOT_HONERING_DISPATCHER_CONTRACT' + } +} + +module.exports = NotHoneringDispatcherContractError diff --git a/core/http/server/dispatcher/collection/builder/index.js b/core/http/server/dispatcher/collection/builder/index.js index 19ca3d1..642051f 100644 --- a/core/http/server/dispatcher/collection/builder/index.js +++ b/core/http/server/dispatcher/collection/builder/index.js @@ -1,61 +1,61 @@ -const -NotHoneringDispatcherContractError = require('./error/not-honering-dispatcher-contract'), -DispatcherCanNotBeResolvedError = require('./error/dispatcher-can-not-be-resolved') - -class ServerDispatcherCollectionBuilder -{ - constructor(path, locator) - { - this.path = path - this.locator = locator - } - - build(route, request, session, viewModel) - { - const dispatchers = [] - - for(const i in route.middleware) - { - const dispatcher = this.createDispatcher(route.middleware[i], route, request, session, viewModel) - dispatchers.push(dispatcher) - } - - const endpoint = this.createDispatcher(route.endpoint, route, request, session, viewModel) - dispatchers.push(endpoint) - - return dispatchers - } - - createDispatcher(pathname, route, request, session, viewModel) - { - const pathnames = - [ - `${pathname}`, - `${this.path.main.dirname}/${pathname}` - ] - - for(const path of pathnames) - { - if(this.path.isResolvable(path)) - { - const - Dispatcher = require(path), - dispatcher = new Dispatcher(route, request, session, this.locator, viewModel) - - if(typeof dispatcher.dispatch !== 'function' - || typeof dispatcher.onError !== 'function') - { - const msg = `dispatcher "${pathname}" is not honering the server dispatcher contract` - throw new NotHoneringDispatcherContractError(msg) - } - - return dispatcher - } - } - - const msg = `dispatcher "${pathname}" can not be resolved in request: ${request.method} -> ${request.url}` - throw new DispatcherCanNotBeResolvedError(msg) - } -} - -module.exports = ServerDispatcherCollectionBuilder +const +NotHoneringDispatcherContractError = require('./error/not-honering-dispatcher-contract'), +DispatcherCanNotBeResolvedError = require('./error/dispatcher-can-not-be-resolved') + +class ServerDispatcherCollectionBuilder +{ + constructor(path, locator) + { + this.path = path + this.locator = locator + } + + build(route, request, session, viewModel) + { + const dispatchers = [] + + for(const i in route.middleware) + { + const dispatcher = this.createDispatcher(route.middleware[i], route, request, session, viewModel) + dispatchers.push(dispatcher) + } + + const endpoint = this.createDispatcher(route.endpoint, route, request, session, viewModel) + dispatchers.push(endpoint) + + return dispatchers + } + + createDispatcher(pathname, route, request, session, viewModel) + { + const pathnames = + [ + `${pathname}`, + `${this.path.main.dirname}/${pathname}` + ] + + for(const path of pathnames) + { + if(this.path.isResolvable(path)) + { + const + Dispatcher = require(path), + dispatcher = new Dispatcher(route, request, session, this.locator, viewModel) + + if(typeof dispatcher.dispatch !== 'function' + || typeof dispatcher.onError !== 'function') + { + const msg = `dispatcher "${pathname}" is not honering the server dispatcher contract` + throw new NotHoneringDispatcherContractError(msg) + } + + return dispatcher + } + } + + const msg = `dispatcher "${pathname}" can not be resolved in request: ${request.method} -> ${request.url}` + throw new DispatcherCanNotBeResolvedError(msg) + } +} + +module.exports = ServerDispatcherCollectionBuilder diff --git a/core/http/server/dispatcher/collection/builder/locator.js b/core/http/server/dispatcher/collection/builder/locator.js index 499d969..3e30191 100644 --- a/core/http/server/dispatcher/collection/builder/locator.js +++ b/core/http/server/dispatcher/collection/builder/locator.js @@ -1,20 +1,20 @@ -const ServerDispatcherCollectionBuilder = require('.') - -class ServerDispatcherCollectionBuilderLocator -{ - constructor(locator) - { - this.locator = locator - } - - locate() - { - const - path = this.locator.locate('core/path'), - dispatcherCollectionBuilder = new ServerDispatcherCollectionBuilder(path, this.locator) - - return dispatcherCollectionBuilder - } -} - -module.exports = ServerDispatcherCollectionBuilderLocator +const ServerDispatcherCollectionBuilder = require('.') + +class ServerDispatcherCollectionBuilderLocator +{ + constructor(locator) + { + this.locator = locator + } + + locate() + { + const + path = this.locator.locate('core/path'), + dispatcherCollectionBuilder = new ServerDispatcherCollectionBuilder(path, this.locator) + + return dispatcherCollectionBuilder + } +} + +module.exports = ServerDispatcherCollectionBuilderLocator diff --git a/core/http/server/dispatcher/error/bad-gateway.js b/core/http/server/dispatcher/error/bad-gateway.js index 5f1480f..df0f173 100644 --- a/core/http/server/dispatcher/error/bad-gateway.js +++ b/core/http/server/dispatcher/error/bad-gateway.js @@ -1,12 +1,12 @@ -const HttpError = require('.') - -class BadGateway extends HttpError -{ - constructor(...args) - { - super(...args) - this.status = 502 - } -} - -module.exports = BadGateway +const HttpError = require('.') + +class BadGateway extends HttpError +{ + constructor(...args) + { + super(...args) + this.status = 502 + } +} + +module.exports = BadGateway diff --git a/core/http/server/dispatcher/error/bad-request.js b/core/http/server/dispatcher/error/bad-request.js index a4fdef9..e078971 100644 --- a/core/http/server/dispatcher/error/bad-request.js +++ b/core/http/server/dispatcher/error/bad-request.js @@ -1,12 +1,12 @@ -const HttpError = require('.') - -class BadRequest extends HttpError -{ - constructor(...args) - { - super(...args) - this.status = 400 - } -} - -module.exports = BadRequest +const HttpError = require('.') + +class BadRequest extends HttpError +{ + constructor(...args) + { + super(...args) + this.status = 400 + } +} + +module.exports = BadRequest diff --git a/core/http/server/dispatcher/error/conflict.js b/core/http/server/dispatcher/error/conflict.js index 2bdd926..344b298 100644 --- a/core/http/server/dispatcher/error/conflict.js +++ b/core/http/server/dispatcher/error/conflict.js @@ -1,12 +1,12 @@ -const HttpError = require('.') - -class Conflict extends HttpError -{ - constructor(...args) - { - super(...args) - this.status = 409 - } -} - -module.exports = Conflict +const HttpError = require('.') + +class Conflict extends HttpError +{ + constructor(...args) + { + super(...args) + this.status = 409 + } +} + +module.exports = Conflict diff --git a/core/http/server/dispatcher/error/forbidden.js b/core/http/server/dispatcher/error/forbidden.js index b176149..67a5600 100644 --- a/core/http/server/dispatcher/error/forbidden.js +++ b/core/http/server/dispatcher/error/forbidden.js @@ -1,12 +1,12 @@ -const HttpError = require('.') - -class Forbidden extends HttpError -{ - constructor(...args) - { - super(...args) - this.status = 403 - } -} - -module.exports = Forbidden +const HttpError = require('.') + +class Forbidden extends HttpError +{ + constructor(...args) + { + super(...args) + this.status = 403 + } +} + +module.exports = Forbidden diff --git a/core/http/server/dispatcher/error/gateway-timeout.js b/core/http/server/dispatcher/error/gateway-timeout.js index 4d9cd4d..ad7f894 100644 --- a/core/http/server/dispatcher/error/gateway-timeout.js +++ b/core/http/server/dispatcher/error/gateway-timeout.js @@ -1,12 +1,12 @@ -const HttpError = require('.') - -class BadGateway extends HttpError -{ - constructor(...args) - { - super(...args) - this.status = 504 - } -} - -module.exports = BadGateway +const HttpError = require('.') + +class BadGateway extends HttpError +{ + constructor(...args) + { + super(...args) + this.status = 504 + } +} + +module.exports = BadGateway diff --git a/core/http/server/dispatcher/error/index.js b/core/http/server/dispatcher/error/index.js index f8ab5c1..7907467 100644 --- a/core/http/server/dispatcher/error/index.js +++ b/core/http/server/dispatcher/error/index.js @@ -1,10 +1,10 @@ -class HttpError extends Error -{ - constructor(...args) - { - super(...args) - this.code = 'E_HTTP_DISPATCHER' - } -} - -module.exports = HttpError +class HttpError extends Error +{ + constructor(...args) + { + super(...args) + this.code = 'E_HTTP_DISPATCHER' + } +} + +module.exports = HttpError diff --git a/core/http/server/dispatcher/error/method-not-allowed.js b/core/http/server/dispatcher/error/method-not-allowed.js index 071e7c7..95af567 100644 --- a/core/http/server/dispatcher/error/method-not-allowed.js +++ b/core/http/server/dispatcher/error/method-not-allowed.js @@ -1,12 +1,12 @@ -const HttpError = require('.') - -class MethodNotAllowed extends HttpError -{ - constructor(...args) - { - super(...args) - this.status = 405 - } -} - -module.exports = MethodNotAllowed +const HttpError = require('.') + +class MethodNotAllowed extends HttpError +{ + constructor(...args) + { + super(...args) + this.status = 405 + } +} + +module.exports = MethodNotAllowed diff --git a/core/http/server/dispatcher/error/not-found.js b/core/http/server/dispatcher/error/not-found.js index 9603784..36d1ad5 100644 --- a/core/http/server/dispatcher/error/not-found.js +++ b/core/http/server/dispatcher/error/not-found.js @@ -1,12 +1,12 @@ -const HttpError = require('.') - -class NotFound extends HttpError -{ - constructor(...args) - { - super(...args) - this.status = 404 - } -} - -module.exports = NotFound +const HttpError = require('.') + +class NotFound extends HttpError +{ + constructor(...args) + { + super(...args) + this.status = 404 + } +} + +module.exports = NotFound diff --git a/core/http/server/dispatcher/error/not-implemented.js b/core/http/server/dispatcher/error/not-implemented.js index 9b32e2b..2e31567 100644 --- a/core/http/server/dispatcher/error/not-implemented.js +++ b/core/http/server/dispatcher/error/not-implemented.js @@ -1,12 +1,12 @@ -const HttpError = require('.') - -class NotImplemented extends HttpError -{ - constructor(...args) - { - super(...args) - this.status = 501 - } -} - -module.exports = NotImplemented +const HttpError = require('.') + +class NotImplemented extends HttpError +{ + constructor(...args) + { + super(...args) + this.status = 501 + } +} + +module.exports = NotImplemented diff --git a/core/http/server/dispatcher/error/request-timeout.js b/core/http/server/dispatcher/error/request-timeout.js index 2f9144d..5415d56 100644 --- a/core/http/server/dispatcher/error/request-timeout.js +++ b/core/http/server/dispatcher/error/request-timeout.js @@ -1,12 +1,12 @@ -const HttpError = require('.') - -class RequestTimeout extends HttpError -{ - constructor(...args) - { - super(...args) - this.status = 408 - } -} - -module.exports = RequestTimeout +const HttpError = require('.') + +class RequestTimeout extends HttpError +{ + constructor(...args) + { + super(...args) + this.status = 408 + } +} + +module.exports = RequestTimeout diff --git a/core/http/server/dispatcher/error/server-error.js b/core/http/server/dispatcher/error/server-error.js index 5ceba02..e5b501e 100644 --- a/core/http/server/dispatcher/error/server-error.js +++ b/core/http/server/dispatcher/error/server-error.js @@ -1,12 +1,12 @@ -const HttpError = require('.') - -class ServerError extends HttpError -{ - constructor(...args) - { - super(...args) - this.status = 500 - } -} - -module.exports = ServerError +const HttpError = require('.') + +class ServerError extends HttpError +{ + constructor(...args) + { + super(...args) + this.status = 500 + } +} + +module.exports = ServerError diff --git a/core/http/server/dispatcher/error/service-unavailable.js b/core/http/server/dispatcher/error/service-unavailable.js index c6939d4..155afb9 100644 --- a/core/http/server/dispatcher/error/service-unavailable.js +++ b/core/http/server/dispatcher/error/service-unavailable.js @@ -1,12 +1,12 @@ -const HttpError = require('.') - -class ServiceUnavailable extends HttpError -{ - constructor(...args) - { - super(...args) - this.status = 503 - } -} - -module.exports = ServiceUnavailable +const HttpError = require('.') + +class ServiceUnavailable extends HttpError +{ + constructor(...args) + { + super(...args) + this.status = 503 + } +} + +module.exports = ServiceUnavailable diff --git a/core/http/server/dispatcher/error/unauthorized.js b/core/http/server/dispatcher/error/unauthorized.js index 973aee4..77984ea 100644 --- a/core/http/server/dispatcher/error/unauthorized.js +++ b/core/http/server/dispatcher/error/unauthorized.js @@ -1,12 +1,12 @@ -const HttpError = require('.') - -class Unauthorized extends HttpError -{ - constructor(...args) - { - super(...args) - this.status = 401 - } -} - -module.exports = Unauthorized +const HttpError = require('.') + +class Unauthorized extends HttpError +{ + constructor(...args) + { + super(...args) + this.status = 401 + } +} + +module.exports = Unauthorized diff --git a/core/http/server/dispatcher/index.js b/core/http/server/dispatcher/index.js index 9997d65..5d7b00b 100644 --- a/core/http/server/dispatcher/index.js +++ b/core/http/server/dispatcher/index.js @@ -1,26 +1,26 @@ -const NotImplementedError = require('./error/not-implemented') - -class HttpDispatcher -{ - constructor(route, request, session, locator, view) - { - this.route = route - this.request = request - this.session = session - this.locator = locator - this.view = view - } - - dispatch() - { - const msg = 'the "dispatch" method is not implemented' - throw new NotImplementedError(msg) - } - - onError(error) - { - throw error - } -} - -module.exports = HttpDispatcher +const NotImplementedError = require('./error/not-implemented') + +class HttpDispatcher +{ + constructor(route, request, session, locator, view) + { + this.route = route + this.request = request + this.session = session + this.locator = locator + this.view = view + } + + dispatch() + { + const msg = 'the "dispatch" method is not implemented' + throw new NotImplementedError(msg) + } + + onError(error) + { + throw error + } +} + +module.exports = HttpDispatcher diff --git a/core/http/server/dispatcher/model-dispatcher.js b/core/http/server/dispatcher/model-dispatcher.js index f3472a3..f68037a 100644 --- a/core/http/server/dispatcher/model-dispatcher.js +++ b/core/http/server/dispatcher/model-dispatcher.js @@ -1,43 +1,43 @@ -const -ServerError = require('./error/server-error'), -Dispatcher = require('.') - -class HttpDispatcherModelDispatcher extends Dispatcher -{ - async dispatch() - { - let model - - try - { - model = this.locator.locate(this.route.model) - } - catch(error) - { - const msg = `the model: "${this.route.model}" could not be located` - throw new ServerError(msg) - } - - if(typeof this.route.command === 'string') - { - const command = this.locator.locate('core/string').composeCamelCase(this.route.command, /[ -_]/) - - if(command in model) - { - this.view.body = await model[command](this.route.dto) - } - else - { - const msg = `the model: "${this.route.model}" does not reccognize the command: "${this.route.command}" -> "${command}"` - throw new ServerError(msg) - } - } - else - { - const msg = `a command must be defined in the route: "${this.route.url}" expected a string, received: "${typeof this.route.command}"` - throw new ServerError(msg) - } - } -} - -module.exports = HttpDispatcherModelDispatcher +const +ServerError = require('./error/server-error'), +Dispatcher = require('.') + +class HttpDispatcherModelDispatcher extends Dispatcher +{ + async dispatch() + { + let model + + try + { + model = this.locator.locate(this.route.model) + } + catch(error) + { + const msg = `the model: "${this.route.model}" could not be located` + throw new ServerError(msg) + } + + if(typeof this.route.command === 'string') + { + const command = this.locator.locate('core/string').composeCamelCase(this.route.command, /[ -_]/) + + if(command in model) + { + this.view.body = await model[command](this.route.dto) + } + else + { + const msg = `the model: "${this.route.model}" does not reccognize the command: "${this.route.command}" -> "${command}"` + throw new ServerError(msg) + } + } + else + { + const msg = `a command must be defined in the route: "${this.route.url}" expected a string, received: "${typeof this.route.command}"` + throw new ServerError(msg) + } + } +} + +module.exports = HttpDispatcherModelDispatcher diff --git a/core/http/server/error/view-contract-not-honered.js b/core/http/server/error/view-contract-not-honered.js index f467807..eb27263 100644 --- a/core/http/server/error/view-contract-not-honered.js +++ b/core/http/server/error/view-contract-not-honered.js @@ -1,10 +1,10 @@ -class ViewContractNotHoneredError extends Error -{ - constructor(...args) - { - super(...args) - this.code = 'E_VIEW_CONTRACT_NOT_HONERED' - } -} - -module.exports = ViewContractNotHoneredError +class ViewContractNotHoneredError extends Error +{ + constructor(...args) + { + super(...args) + this.code = 'E_VIEW_CONTRACT_NOT_HONERED' + } +} + +module.exports = ViewContractNotHoneredError diff --git a/core/http/server/index.js b/core/http/server/index.js index 045701a..c365eb1 100644 --- a/core/http/server/index.js +++ b/core/http/server/index.js @@ -1,190 +1,190 @@ -const ViewContractNotHoneredError = require('./error/view-contract-not-honered') - -class HttpServer -{ - /** - * @param {http.Server} server - */ - constructor(server, requestBuilder, sessionBuilder, routeBuilder, - dispatcherCollectionBuilder, dispatcherChain, configuration, - locator, eventbus, domainFactory) - { - this.server = server - this.requestBuilder = requestBuilder - this.sessionBuilder = sessionBuilder - this.routeBuilder = routeBuilder - this.dispatcherCollectionBuilder = dispatcherCollectionBuilder - this.dispatcherChain = dispatcherChain - this.configuration = configuration - this.locator = locator - this.eventbus = eventbus - this.domainFactory = domainFactory - } - - listen(...args) - { - this.server.listen(...args) - } - - onListening(done) - { - return this.server.listening - ? done() - : this.server.once('listening', done) - } - - close() - { - return new Promise((accept, reject) => - this.server.close((error) => - error - ? reject(error) - : accept())) - } - - onRequest(input, output) - { - const - domain = this.domainFactory.create(), - onError = this.onError.bind(this, input, output, domain) - - domain.add(input) - domain.add(output) - - domain.on('error', onError) - input.on('aborted', this.onAborted.bind(this, output)) - output.on('timeout', this.onTimeout.bind(this, output)) - output.on('finish', this.onFinish .bind(this, input, output, domain)) - - domain.run(() => this.dispatch(input, output, domain).catch(onError)) - } - - onAborted(output) - { - output.end() - } - - onFinish(input, output, domain) - { - let - emitters = 0, - timeouts = 0 - - for(const member of domain.members) - 'removeAllListeners' in member - ? emitters++ - : timeouts++ - - if(domain.members.length > 2) - { - const msg = - [ - `Finished session did not clear all domain members!`, - `Expected: 2 (response and request)`, - `Recieved: ${domain.members.length}`, - `Emitters: ${emitters}`, - `Timeouts: ${timeouts}` - ].join('\n') - - this.eventbus.emit('core.warning', msg) - } - - domain.exit() - domain.removeAllListeners() - input .removeAllListeners() - output.removeAllListeners() - } - - onTimeout(output) - { - output.writeHead(408) - output.end('Request Timeout') - } - - onError(input, output, domain, error) - { - switch(error.code) - { - case 'E_HTTP_SERVER_ROUTE_BUILDER_INVALID_DTO': - { - output.writeHead(400) - output.end('Bad request\n---\n' + error.message) - break - } - case 'E_JSON_PARSE_ERROR': - { - output.writeHead(400) - output.end('Bad request\nInvalid JSON format\n---\n' + error.message) - break - } - case 'E_HTTP_DISPATCHER': - { - output.writeHead(error.status) - output.end(error.message) - break - } - case 'E_NO_ENDPOINT_DEFINED_IN_ROUTE': - { - this.eventbus.emit('core.error', error) - - output.writeHead(404) - output.end('Endpoint not found') - break - } - default: - { - this.eventbus.emit('core.error', error) - - output.writeHead(500) - output.end('Internal server error') - break - } - } - } - - async dispatch(input, output, domain) - { - const - routes = this.configuration.find('core.http.server.routes'), - session = await this.sessionBuilder.build(input, output, domain), - request = await this.requestBuilder.build(input), - route = await this.routeBuilder.build(routes, request), - viewModel = this.createViewModel() - - const dispatchers = await this.dispatcherCollectionBuilder.build(route, request, session, viewModel) - await this.dispatcherChain.dispatch(dispatchers) - - if(!output.finished) - { - const - viewType = viewModel.meta.view || route.view || 'core/http/server/view', - view = this.locator.locate(viewType) - - if(typeof view.write !== 'function') - { - const msg = `The service "${viewType}" does not honer the view contract` - throw new ViewContractNotHoneredError(msg) - } - - await view.write(output, viewModel, route) - } - } - - /** - * Allowing the body to be set, in order to be able to have arrays and strings as a response body - * @returns {Object} - */ - createViewModel() - { - const viewModel = - { - body : {}, - headers : {}, - meta : {} - } - - return viewModel - } -} - -module.exports = HttpServer +const ViewContractNotHoneredError = require('./error/view-contract-not-honered') + +class HttpServer +{ + /** + * @param {http.Server} server + */ + constructor(server, requestBuilder, sessionBuilder, routeBuilder, + dispatcherCollectionBuilder, dispatcherChain, configuration, + locator, eventbus, domainFactory) + { + this.server = server + this.requestBuilder = requestBuilder + this.sessionBuilder = sessionBuilder + this.routeBuilder = routeBuilder + this.dispatcherCollectionBuilder = dispatcherCollectionBuilder + this.dispatcherChain = dispatcherChain + this.configuration = configuration + this.locator = locator + this.eventbus = eventbus + this.domainFactory = domainFactory + } + + listen(...args) + { + this.server.listen(...args) + } + + onListening(done) + { + return this.server.listening + ? done() + : this.server.once('listening', done) + } + + close() + { + return new Promise((accept, reject) => + this.server.close((error) => + error + ? reject(error) + : accept())) + } + + onRequest(input, output) + { + const + domain = this.domainFactory.create(), + onError = this.onError.bind(this, input, output, domain) + + domain.add(input) + domain.add(output) + + domain.on('error', onError) + input.on('aborted', this.onAborted.bind(this, output)) + output.on('timeout', this.onTimeout.bind(this, output)) + output.on('finish', this.onFinish .bind(this, input, output, domain)) + + domain.run(() => this.dispatch(input, output, domain).catch(onError)) + } + + onAborted(output) + { + output.end() + } + + onFinish(input, output, domain) + { + let + emitters = 0, + timeouts = 0 + + for(const member of domain.members) + 'removeAllListeners' in member + ? emitters++ + : timeouts++ + + if(domain.members.length > 2) + { + const msg = + [ + `Finished session did not clear all domain members!`, + `Expected: 2 (response and request)`, + `Recieved: ${domain.members.length}`, + `Emitters: ${emitters}`, + `Timeouts: ${timeouts}` + ].join('\n') + + this.eventbus.emit('core.warning', msg) + } + + domain.exit() + domain.removeAllListeners() + input .removeAllListeners() + output.removeAllListeners() + } + + onTimeout(output) + { + output.writeHead(408) + output.end('Request Timeout') + } + + onError(input, output, domain, error) + { + switch(error.code) + { + case 'E_HTTP_SERVER_ROUTE_BUILDER_INVALID_DTO': + { + output.writeHead(400) + output.end('Bad request\n---\n' + error.message) + break + } + case 'E_JSON_PARSE_ERROR': + { + output.writeHead(400) + output.end('Bad request\nInvalid JSON format\n---\n' + error.message) + break + } + case 'E_HTTP_DISPATCHER': + { + output.writeHead(error.status) + output.end(error.message) + break + } + case 'E_NO_ENDPOINT_DEFINED_IN_ROUTE': + { + this.eventbus.emit('core.error', error) + + output.writeHead(404) + output.end('Endpoint not found') + break + } + default: + { + this.eventbus.emit('core.error', error) + + output.writeHead(500) + output.end('Internal server error') + break + } + } + } + + async dispatch(input, output, domain) + { + const + routes = this.configuration.find('core.http.server.routes'), + session = await this.sessionBuilder.build(input, output, domain), + request = await this.requestBuilder.build(input), + route = await this.routeBuilder.build(routes, request), + viewModel = this.createViewModel() + + const dispatchers = await this.dispatcherCollectionBuilder.build(route, request, session, viewModel) + await this.dispatcherChain.dispatch(dispatchers) + + if(!output.finished) + { + const + viewType = viewModel.meta.view || route.view || 'core/http/server/view', + view = this.locator.locate(viewType) + + if(typeof view.write !== 'function') + { + const msg = `The service "${viewType}" does not honer the view contract` + throw new ViewContractNotHoneredError(msg) + } + + await view.write(output, viewModel, route) + } + } + + /** + * Allowing the body to be set, in order to be able to have arrays and strings as a response body + * @returns {Object} + */ + createViewModel() + { + const viewModel = + { + body : {}, + headers : {}, + meta : {} + } + + return viewModel + } +} + +module.exports = HttpServer diff --git a/core/http/server/locator.js b/core/http/server/locator.js index 4f2a3fd..a1461eb 100644 --- a/core/http/server/locator.js +++ b/core/http/server/locator.js @@ -1,34 +1,34 @@ -const HttpServer = require('.') - -class HttpServerLocator -{ - constructor(locator) - { - this.locator = locator - } - - locate() - { - const - configuration = this.locator.locate('core/configuration'), - serverOptions = configuration.find('core.http.server.options'), - server = require('http').createServer(serverOptions), - requestBuilder = this.locator.locate('core/http/server/request/builder'), - sessionFactory = this.locator.locate('core/http/server/session/builder'), - routeBuilder = this.locator.locate('core/http/server/route/builder'), - dispatcherCollectionBuilder = this.locator.locate('core/http/server/dispatcher/collection/builder'), - dispatcherChain = this.locator.locate('core/http/server/dispatcher/chain'), - eventbus = this.locator.locate('core/eventbus'), - domainFactory = require('domain'), - httpServer = new HttpServer(server, requestBuilder, sessionFactory, routeBuilder, - dispatcherCollectionBuilder, dispatcherChain, configuration, - this.locator, eventbus, domainFactory) - - server.timeout = configuration.find('core.http.server.timeout') - server.on('request', httpServer.onRequest.bind(httpServer)) - - return httpServer - } -} - -module.exports = HttpServerLocator +const HttpServer = require('.') + +class HttpServerLocator +{ + constructor(locator) + { + this.locator = locator + } + + locate() + { + const + configuration = this.locator.locate('core/configuration'), + serverOptions = configuration.find('core.http.server.options'), + server = require('http').createServer(serverOptions), + requestBuilder = this.locator.locate('core/http/server/request/builder'), + sessionFactory = this.locator.locate('core/http/server/session/builder'), + routeBuilder = this.locator.locate('core/http/server/route/builder'), + dispatcherCollectionBuilder = this.locator.locate('core/http/server/dispatcher/collection/builder'), + dispatcherChain = this.locator.locate('core/http/server/dispatcher/chain'), + eventbus = this.locator.locate('core/eventbus'), + domainFactory = require('domain'), + httpServer = new HttpServer(server, requestBuilder, sessionFactory, routeBuilder, + dispatcherCollectionBuilder, dispatcherChain, configuration, + this.locator, eventbus, domainFactory) + + server.timeout = configuration.find('core.http.server.timeout') + server.on('request', httpServer.onRequest.bind(httpServer)) + + return httpServer + } +} + +module.exports = HttpServerLocator diff --git a/core/http/server/request/builder/index.js b/core/http/server/request/builder/index.js index e303b80..300eb3a 100644 --- a/core/http/server/request/builder/index.js +++ b/core/http/server/request/builder/index.js @@ -1,73 +1,73 @@ -const -urlParser = require('url').parse, -querystring = require('querystring') - -class HttpRequestBuilder -{ - constructor(deepfreeze) - { - this.deepfreeze = deepfreeze - } - - async build(input) - { - const - parsedUrl = urlParser(input.url, true), - headers = this.mapHeaders(input.headers), - method = input.method.toUpperCase(), - url = parsedUrl.pathname.replace(/\/+$/g, ''), - query = parsedUrl.query, - body = await this.fetchBody(input, headers), - request = { headers, method, url, query, body } - - return this.deepfreeze.freeze(request) - } - - mapHeaders(headers) - { - const mapped = {} - - for(const key in headers) - { - const lowerCaseKey = key.toLowerCase() - mapped[lowerCaseKey] = headers[key] - } - - return mapped - } - - async fetchBody(stream, headers) - { - return new Promise((accept, reject) => - { - let body = '' - - stream.on('error', reject) - stream.on('data', (data) => body += data) - stream.on('end', () => this.parseBody(headers['content-type'], body).then(accept).catch(reject)) - }) - } - - async parseBody(contentType, body) - { - switch((contentType || '').split(';').shift()) - { - case 'application/json': - try - { - return JSON.parse(body || '{}') - } - catch(err) - { - const error = new Error(err.message) - error.msg = 'E_JSON_PARSE_ERROR' - throw error - } - - default: - return querystring.parse(body) - } - } -} - -module.exports = HttpRequestBuilder +const +urlParser = require('url').parse, +querystring = require('querystring') + +class HttpRequestBuilder +{ + constructor(deepfreeze) + { + this.deepfreeze = deepfreeze + } + + async build(input) + { + const + parsedUrl = urlParser(input.url, true), + headers = this.mapHeaders(input.headers), + method = input.method.toUpperCase(), + url = parsedUrl.pathname.replace(/\/+$/g, ''), + query = parsedUrl.query, + body = await this.fetchBody(input, headers), + request = { headers, method, url, query, body } + + return this.deepfreeze.freeze(request) + } + + mapHeaders(headers) + { + const mapped = {} + + for(const key in headers) + { + const lowerCaseKey = key.toLowerCase() + mapped[lowerCaseKey] = headers[key] + } + + return mapped + } + + async fetchBody(stream, headers) + { + return new Promise((accept, reject) => + { + let body = '' + + stream.on('error', reject) + stream.on('data', (data) => body += data) + stream.on('end', () => this.parseBody(headers['content-type'], body).then(accept).catch(reject)) + }) + } + + async parseBody(contentType, body) + { + switch((contentType || '').split(';').shift()) + { + case 'application/json': + try + { + return JSON.parse(body || '{}') + } + catch(err) + { + const error = new Error(err.message) + error.msg = 'E_JSON_PARSE_ERROR' + throw error + } + + default: + return querystring.parse(body) + } + } +} + +module.exports = HttpRequestBuilder diff --git a/core/http/server/request/builder/locator.js b/core/http/server/request/builder/locator.js index 945f120..ef5703a 100644 --- a/core/http/server/request/builder/locator.js +++ b/core/http/server/request/builder/locator.js @@ -1,20 +1,20 @@ -const HttpRequestBuilder = require('.') - -class HttpRequestBuilderLocator -{ - constructor(locator) - { - this.locator = locator - } - - locate() - { - const - deepfreeze = this.locator.locate('core/deepfreeze'), - httpRequestBuilder = new HttpRequestBuilder(deepfreeze) - - return httpRequestBuilder - } -} - -module.exports = HttpRequestBuilderLocator +const HttpRequestBuilder = require('.') + +class HttpRequestBuilderLocator +{ + constructor(locator) + { + this.locator = locator + } + + locate() + { + const + deepfreeze = this.locator.locate('core/deepfreeze'), + httpRequestBuilder = new HttpRequestBuilder(deepfreeze) + + return httpRequestBuilder + } +} + +module.exports = HttpRequestBuilderLocator diff --git a/core/http/server/route/builder/bootstrap/index.js b/core/http/server/route/builder/bootstrap/index.js index 45b1f93..a35621e 100644 --- a/core/http/server/route/builder/bootstrap/index.js +++ b/core/http/server/route/builder/bootstrap/index.js @@ -1,23 +1,23 @@ -class HttpServerRouteBuilderBootstrap -{ - constructor(configuration, locator) - { - this.configuration = configuration - this.locator = locator - } - - bootstrap() - { - const - routeBuilder = this.locator.locate('core/http/server/route/builder'), - dtoBuilders = this.configuration.find('core.http.server.route.builder.dto.builders') - - for(const dtoBuilderName in dtoBuilders) - { - const dtoBuilder = this.locator.locate(dtoBuilders[dtoBuilderName]) - routeBuilder.addDtoBuilder(dtoBuilder) - } - } -} - -module.exports = HttpServerRouteBuilderBootstrap +class HttpServerRouteBuilderBootstrap +{ + constructor(configuration, locator) + { + this.configuration = configuration + this.locator = locator + } + + bootstrap() + { + const + routeBuilder = this.locator.locate('core/http/server/route/builder'), + dtoBuilders = this.configuration.find('core.http.server.route.builder.dto.builders') + + for(const dtoBuilderName in dtoBuilders) + { + const dtoBuilder = this.locator.locate(dtoBuilders[dtoBuilderName]) + routeBuilder.addDtoBuilder(dtoBuilder) + } + } +} + +module.exports = HttpServerRouteBuilderBootstrap diff --git a/core/http/server/route/builder/bootstrap/locator.js b/core/http/server/route/builder/bootstrap/locator.js index a28547d..d2288fe 100644 --- a/core/http/server/route/builder/bootstrap/locator.js +++ b/core/http/server/route/builder/bootstrap/locator.js @@ -1,17 +1,17 @@ -const HttpServerRouteBuilderBootstrap = require('.') - -class HttpServerRouteBuilderBootstrapLocator -{ - constructor(locator) - { - this.locator = locator - } - - locate() - { - const configuration = this.locator.locate('core/configuration') - return new HttpServerRouteBuilderBootstrap(configuration, this.locator) - } -} - -module.exports = HttpServerRouteBuilderBootstrapLocator +const HttpServerRouteBuilderBootstrap = require('.') + +class HttpServerRouteBuilderBootstrapLocator +{ + constructor(locator) + { + this.locator = locator + } + + locate() + { + const configuration = this.locator.locate('core/configuration') + return new HttpServerRouteBuilderBootstrap(configuration, this.locator) + } +} + +module.exports = HttpServerRouteBuilderBootstrapLocator diff --git a/core/http/server/route/builder/dto/builder/index.js b/core/http/server/route/builder/dto/builder/index.js index d57d2ba..ee8d8cf 100644 --- a/core/http/server/route/builder/dto/builder/index.js +++ b/core/http/server/route/builder/dto/builder/index.js @@ -1,11 +1,11 @@ -/** - * @interface HttpServerRouteBuilderDtoBuilder - */ - -/** - * @function HttpServerRouteBuilderDtoBuilder#build - * @param {Object} dto - The data transfer object that will be altered - * @param {Object} route - The composed route - * @param {Object} request - The composed request - * @returns {void} - */ +/** + * @interface HttpServerRouteBuilderDtoBuilder + */ + +/** + * @function HttpServerRouteBuilderDtoBuilder#build + * @param {Object} dto - The data transfer object that will be altered + * @param {Object} route - The composed route + * @param {Object} request - The composed request + * @returns {void} + */ diff --git a/core/http/server/route/builder/dto/builder/request-body/index.js b/core/http/server/route/builder/dto/builder/request-body/index.js index 49ee1ba..e1c727b 100644 --- a/core/http/server/route/builder/dto/builder/request-body/index.js +++ b/core/http/server/route/builder/dto/builder/request-body/index.js @@ -1,12 +1,12 @@ -class HttpServerRouteBuilderDtoBuilderRequestBody -{ - build(dto, route, request) - { - for(const key in request.body) - { - dto[key] = request.body[key] - } - } -} - -module.exports = HttpServerRouteBuilderDtoBuilderRequestBody +class HttpServerRouteBuilderDtoBuilderRequestBody +{ + build(dto, route, request) + { + for(const key in request.body) + { + dto[key] = request.body[key] + } + } +} + +module.exports = HttpServerRouteBuilderDtoBuilderRequestBody diff --git a/core/http/server/route/builder/dto/builder/request-body/locator.js b/core/http/server/route/builder/dto/builder/request-body/locator.js index 91ef2d0..8fac582 100644 --- a/core/http/server/route/builder/dto/builder/request-body/locator.js +++ b/core/http/server/route/builder/dto/builder/request-body/locator.js @@ -1,11 +1,11 @@ -const HttpServerRouteBuilderDtoBuilderRequestBody = require('.') - -class HttpServerRouteBuilderDtoBuilderRequestBodyLocator -{ - locate() - { - return new HttpServerRouteBuilderDtoBuilderRequestBody - } -} - -module.exports = HttpServerRouteBuilderDtoBuilderRequestBodyLocator +const HttpServerRouteBuilderDtoBuilderRequestBody = require('.') + +class HttpServerRouteBuilderDtoBuilderRequestBodyLocator +{ + locate() + { + return new HttpServerRouteBuilderDtoBuilderRequestBody + } +} + +module.exports = HttpServerRouteBuilderDtoBuilderRequestBodyLocator diff --git a/core/http/server/route/builder/dto/builder/request-query/index.js b/core/http/server/route/builder/dto/builder/request-query/index.js index e4ab036..7ac7674 100644 --- a/core/http/server/route/builder/dto/builder/request-query/index.js +++ b/core/http/server/route/builder/dto/builder/request-query/index.js @@ -1,12 +1,12 @@ -class HttpServerRouteBuilderDtoBuilderRequestQuery -{ - build(dto, route, request) - { - for(const key in request.query) - { - dto[key] = request.query[key] - } - } -} - -module.exports = HttpServerRouteBuilderDtoBuilderRequestQuery +class HttpServerRouteBuilderDtoBuilderRequestQuery +{ + build(dto, route, request) + { + for(const key in request.query) + { + dto[key] = request.query[key] + } + } +} + +module.exports = HttpServerRouteBuilderDtoBuilderRequestQuery diff --git a/core/http/server/route/builder/dto/builder/request-query/locator.js b/core/http/server/route/builder/dto/builder/request-query/locator.js index 91ef2d0..8fac582 100644 --- a/core/http/server/route/builder/dto/builder/request-query/locator.js +++ b/core/http/server/route/builder/dto/builder/request-query/locator.js @@ -1,11 +1,11 @@ -const HttpServerRouteBuilderDtoBuilderRequestBody = require('.') - -class HttpServerRouteBuilderDtoBuilderRequestBodyLocator -{ - locate() - { - return new HttpServerRouteBuilderDtoBuilderRequestBody - } -} - -module.exports = HttpServerRouteBuilderDtoBuilderRequestBodyLocator +const HttpServerRouteBuilderDtoBuilderRequestBody = require('.') + +class HttpServerRouteBuilderDtoBuilderRequestBodyLocator +{ + locate() + { + return new HttpServerRouteBuilderDtoBuilderRequestBody + } +} + +module.exports = HttpServerRouteBuilderDtoBuilderRequestBodyLocator diff --git a/core/http/server/route/builder/dto/builder/request-url/index.js b/core/http/server/route/builder/dto/builder/request-url/index.js index 959e6a0..aa33821 100644 --- a/core/http/server/route/builder/dto/builder/request-url/index.js +++ b/core/http/server/route/builder/dto/builder/request-url/index.js @@ -1,22 +1,22 @@ -class HttpServerRouteBuilderDtoBuilderRequestUrl -{ - build(dto, route, request) - { - const - requestUrl = request.url.split('/'), - routeUrl = route.url.split('/') - - for(const i in routeUrl) - { - const segment = routeUrl[i] - - if(segment.startsWith(':')) - { - const key = segment.split('=').shift().slice(1) - dto[key] = requestUrl[i] - } - } - } -} - -module.exports = HttpServerRouteBuilderDtoBuilderRequestUrl +class HttpServerRouteBuilderDtoBuilderRequestUrl +{ + build(dto, route, request) + { + const + requestUrl = request.url.split('/'), + routeUrl = route.url.split('/') + + for(const i in routeUrl) + { + const segment = routeUrl[i] + + if(segment.startsWith(':')) + { + const key = segment.split('=').shift().slice(1) + dto[key] = requestUrl[i] + } + } + } +} + +module.exports = HttpServerRouteBuilderDtoBuilderRequestUrl diff --git a/core/http/server/route/builder/dto/builder/request-url/locator.js b/core/http/server/route/builder/dto/builder/request-url/locator.js index 485f883..e8937d5 100644 --- a/core/http/server/route/builder/dto/builder/request-url/locator.js +++ b/core/http/server/route/builder/dto/builder/request-url/locator.js @@ -1,11 +1,11 @@ -const HttpServerRouteBuilderDtoBuilderRequestUrl = require('.') - -class HttpServerRouteBuilderDtoBuilderRequestUrlLocator -{ - locate() - { - return new HttpServerRouteBuilderDtoBuilderRequestUrl - } -} - -module.exports = HttpServerRouteBuilderDtoBuilderRequestUrlLocator +const HttpServerRouteBuilderDtoBuilderRequestUrl = require('.') + +class HttpServerRouteBuilderDtoBuilderRequestUrlLocator +{ + locate() + { + return new HttpServerRouteBuilderDtoBuilderRequestUrl + } +} + +module.exports = HttpServerRouteBuilderDtoBuilderRequestUrlLocator diff --git a/core/http/server/route/builder/error/dto-builder-contract-not-honered.js b/core/http/server/route/builder/error/dto-builder-contract-not-honered.js index 09b5b2e..2c87048 100644 --- a/core/http/server/route/builder/error/dto-builder-contract-not-honered.js +++ b/core/http/server/route/builder/error/dto-builder-contract-not-honered.js @@ -1,10 +1,10 @@ -class DtoBuilderContractNotHoneredError extends TypeError -{ - constructor(...args) - { - super(...args) - this.code = 'E_DTO_BUILDER_CONTRACT_NOT_HONERED' - } -} - -module.exports = DtoBuilderContractNotHoneredError +class DtoBuilderContractNotHoneredError extends TypeError +{ + constructor(...args) + { + super(...args) + this.code = 'E_DTO_BUILDER_CONTRACT_NOT_HONERED' + } +} + +module.exports = DtoBuilderContractNotHoneredError diff --git a/core/http/server/route/builder/error/invalid-dto.js b/core/http/server/route/builder/error/invalid-dto.js index 2099fc4..1695e1b 100644 --- a/core/http/server/route/builder/error/invalid-dto.js +++ b/core/http/server/route/builder/error/invalid-dto.js @@ -1,10 +1,10 @@ -class InvalidDtoTypeError extends TypeError -{ - constructor(...args) - { - super(...args) - this.code = 'E_HTTP_SERVER_ROUTE_BUILDER_INVALID_DTO' - } -} - -module.exports = InvalidDtoTypeError +class InvalidDtoTypeError extends TypeError +{ + constructor(...args) + { + super(...args) + this.code = 'E_HTTP_SERVER_ROUTE_BUILDER_INVALID_DTO' + } +} + +module.exports = InvalidDtoTypeError diff --git a/core/http/server/route/builder/error/invalid-route-input.js b/core/http/server/route/builder/error/invalid-route-input.js index 47d95fd..fdbfd4e 100644 --- a/core/http/server/route/builder/error/invalid-route-input.js +++ b/core/http/server/route/builder/error/invalid-route-input.js @@ -1,10 +1,10 @@ -class InvalidRouteInputError extends TypeError -{ - constructor(...args) - { - super(...args) - this.code = 'E_HTTP_SERVER_ROUTE_BUILDER_INVALID_ROUTE_INPUT' - } -} - -module.exports = InvalidRouteInputError +class InvalidRouteInputError extends TypeError +{ + constructor(...args) + { + super(...args) + this.code = 'E_HTTP_SERVER_ROUTE_BUILDER_INVALID_ROUTE_INPUT' + } +} + +module.exports = InvalidRouteInputError diff --git a/core/http/server/route/builder/error/invalid-route.js b/core/http/server/route/builder/error/invalid-route.js index af4c71e..78bb8f6 100644 --- a/core/http/server/route/builder/error/invalid-route.js +++ b/core/http/server/route/builder/error/invalid-route.js @@ -1,10 +1,10 @@ -class InvalidRouteError extends TypeError -{ - constructor(...args) - { - super(...args) - this.code = 'E_HTTP_SERVER_ROUTE_BUILDER_INVALID_ROUTE' - } -} - -module.exports = InvalidRouteError +class InvalidRouteError extends TypeError +{ + constructor(...args) + { + super(...args) + this.code = 'E_HTTP_SERVER_ROUTE_BUILDER_INVALID_ROUTE' + } +} + +module.exports = InvalidRouteError diff --git a/core/http/server/route/builder/error/no-endpoint-defined.js b/core/http/server/route/builder/error/no-endpoint-defined.js index f3f52ab..23a060c 100644 --- a/core/http/server/route/builder/error/no-endpoint-defined.js +++ b/core/http/server/route/builder/error/no-endpoint-defined.js @@ -1,10 +1,10 @@ -class NoEndpointDefinedInRouteError extends Error -{ - constructor(...args) - { - super(...args) - this.code = 'E_NO_ENDPOINT_DEFINED_IN_ROUTE' - } -} - -module.exports = NoEndpointDefinedInRouteError +class NoEndpointDefinedInRouteError extends Error +{ + constructor(...args) + { + super(...args) + this.code = 'E_NO_ENDPOINT_DEFINED_IN_ROUTE' + } +} + +module.exports = NoEndpointDefinedInRouteError diff --git a/core/http/server/route/builder/error/no-route-found.js b/core/http/server/route/builder/error/no-route-found.js index 822a558..d225a19 100644 --- a/core/http/server/route/builder/error/no-route-found.js +++ b/core/http/server/route/builder/error/no-route-found.js @@ -1,10 +1,10 @@ -class NoRouteFoundError extends Error -{ - constructor(...args) - { - super(...args) - this.code = 'E_HTTP_SERVER_ROUTE_BUILDER_NO_ROUTE_FOUND' - } -} - -module.exports = NoRouteFoundError +class NoRouteFoundError extends Error +{ + constructor(...args) + { + super(...args) + this.code = 'E_HTTP_SERVER_ROUTE_BUILDER_NO_ROUTE_FOUND' + } +} + +module.exports = NoRouteFoundError diff --git a/core/http/server/route/builder/error/routes-invalid-type.js b/core/http/server/route/builder/error/routes-invalid-type.js index d4f30a4..829138e 100644 --- a/core/http/server/route/builder/error/routes-invalid-type.js +++ b/core/http/server/route/builder/error/routes-invalid-type.js @@ -1,10 +1,10 @@ -class RoutesInvalidTypeError extends TypeError -{ - constructor(...args) - { - super(...args) - this.code = 'E_ROUTES_INVALID_TYPE' - } -} - -module.exports = RoutesInvalidTypeError +class RoutesInvalidTypeError extends TypeError +{ + constructor(...args) + { + super(...args) + this.code = 'E_ROUTES_INVALID_TYPE' + } +} + +module.exports = RoutesInvalidTypeError diff --git a/core/http/server/route/builder/index.js b/core/http/server/route/builder/index.js index 99376f3..48e6774 100644 --- a/core/http/server/route/builder/index.js +++ b/core/http/server/route/builder/index.js @@ -1,198 +1,198 @@ -const -DtoBuilderContractNotHoneredError = require('./error/dto-builder-contract-not-honered'), -RoutesInvalidTypeError = require('./error/routes-invalid-type'), -InvalidRouteInputError = require('./error/invalid-route-input'), -InvalidDtoError = require('./error/invalid-dto'), -NoRouteFoundError = require('./error/no-route-found'), -NoEndpointDefinedError = require('./error/no-endpoint-defined'), -InvalidRouteError = require('./error/invalid-route') - -class HttpServerRouteBuilder -{ - constructor(deepmerge, deepclone, composer) - { - this.dtoBuilders = [] - this.deepmerge = deepmerge - this.deepclone = deepclone - this.composer = composer - } - - /** - * @param {Array} routes - * @param {Object} request - */ - build(routes, request) - { - if(typeof routes !== 'object') - { - const msg = 'routes must be built from an object' - throw new RoutesInvalidTypeError(msg) - } - - const - validRoutes = this.fetchValidRoutes(routes, request), - validRoutesClone = this.deepclone.clone(validRoutes), - route = this.deepmerge.merge({}, ...validRoutesClone) - - if(!validRoutesClone.length) - { - const msg = `Could not find a matching route for the request: ${request.method} -> ${request.url}` - throw new NoRouteFoundError(msg) - } - - if(!route.endpoint) - { - const msg = `No endpoint defined in route for the request: ${request.method} -> ${request.url}` - throw new NoEndpointDefinedError(msg) - } - - if('input' in route === false) - { - const msg = `route requires a defintion of an input schema, "false" is an acceptable value: ${request.method} -> ${request.url}` - throw new InvalidRouteInputError(msg) - } - - try - { - if(route.input) - { - route.dto = this.composeDto(request, route) - } - - return route - } - catch(error) - { - throw new InvalidDtoError(error.message) - } - } - - /** - * @param {Array} routes - * @param {Object} request - */ - fetchValidRoutes(routes, request) - { - const validRoutes = [] - - for(const name in routes) - { - const - route = routes[name], - url = route.url && new RegExp(`^${route.url.split('/').map(this.mapSegments).join('/').replace(/\/+$/g, '')}$`), - method = route.method && new RegExp(`^${route.method}$`, 'i') - - if(request.url .match(url) - && request.method .match(method)) - { - if(route.headers) - { - if(typeof route.headers !== 'object') - { - const msg = `the route headers must be of type object, found: ${typeof route.headers}, in route: ${name}` - throw new InvalidRouteError(msg) - } - - // validate that all headers match - const isHeadersValid = Object.keys(route.headers).every((header) => - { - const headerRegExp = new RegExp(`^${route.headers[header]}$`, 'i') - - if(header in request.headers - && request.headers[header].match(headerRegExp)) - { - return true - } - }) - - if(isHeadersValid) - { - validRoutes.push(route) - - // when an endpoint has been found, the route is terminated - if(route.endpoint) - { - return validRoutes - } - } - } - else - { - validRoutes.push(route) - - // when an endpoint has been found, the route is terminated - if(route.endpoint) - { - return validRoutes - } - } - } - } - - return validRoutes - } - - /** - * @private - * @param {string} segment - */ - mapSegments(segment) - { - if(segment.startsWith(':')) - { - if(segment.includes('=')) - { - return segment.split('=').pop() - } - else - { - return '[^/]+' - } - } - else - { - return segment - } - } - - /** - * @param {HttpServerRouteBuilderDtoBuilder} dtoBuilder - */ - addDtoBuilder(dtoBuilder) - { - if(typeof dtoBuilder.build !== 'function') - { - const msg = 'Expected "dtoBuilder" to have a build function' - throw new DtoBuilderContractNotHoneredError(msg) - } - - return this.dtoBuilders.push(dtoBuilder) - } - - /** - * @param {number} index - */ - removeDtoBuilder(index) - { - return delete this.dtoBuilders[index - 1] - } - - /** - * @param {Object} request - * @param {Object} route - */ - composeDto(request, route) - { - const dto = {} - - for(const index in this.dtoBuilders) - { - const dtoBuilder = this.dtoBuilders[index] - dtoBuilder.build(dto, route, request) - } - - return this.composer.compose(route.input, dto) - } -} - -module.exports = HttpServerRouteBuilder +const +DtoBuilderContractNotHoneredError = require('./error/dto-builder-contract-not-honered'), +RoutesInvalidTypeError = require('./error/routes-invalid-type'), +InvalidRouteInputError = require('./error/invalid-route-input'), +InvalidDtoError = require('./error/invalid-dto'), +NoRouteFoundError = require('./error/no-route-found'), +NoEndpointDefinedError = require('./error/no-endpoint-defined'), +InvalidRouteError = require('./error/invalid-route') + +class HttpServerRouteBuilder +{ + constructor(deepmerge, deepclone, composer) + { + this.dtoBuilders = [] + this.deepmerge = deepmerge + this.deepclone = deepclone + this.composer = composer + } + + /** + * @param {Array} routes + * @param {Object} request + */ + build(routes, request) + { + if(typeof routes !== 'object') + { + const msg = 'routes must be built from an object' + throw new RoutesInvalidTypeError(msg) + } + + const + validRoutes = this.fetchValidRoutes(routes, request), + validRoutesClone = this.deepclone.clone(validRoutes), + route = this.deepmerge.merge({}, ...validRoutesClone) + + if(!validRoutesClone.length) + { + const msg = `Could not find a matching route for the request: ${request.method} -> ${request.url}` + throw new NoRouteFoundError(msg) + } + + if(!route.endpoint) + { + const msg = `No endpoint defined in route for the request: ${request.method} -> ${request.url}` + throw new NoEndpointDefinedError(msg) + } + + if('input' in route === false) + { + const msg = `route requires a defintion of an input schema, "false" is an acceptable value: ${request.method} -> ${request.url}` + throw new InvalidRouteInputError(msg) + } + + try + { + if(route.input) + { + route.dto = this.composeDto(request, route) + } + + return route + } + catch(error) + { + throw new InvalidDtoError(error.message) + } + } + + /** + * @param {Array} routes + * @param {Object} request + */ + fetchValidRoutes(routes, request) + { + const validRoutes = [] + + for(const name in routes) + { + const + route = routes[name], + url = route.url && new RegExp(`^${route.url.split('/').map(this.mapSegments).join('/').replace(/\/+$/g, '')}$`), + method = route.method && new RegExp(`^${route.method}$`, 'i') + + if(request.url .match(url) + && request.method .match(method)) + { + if(route.headers) + { + if(typeof route.headers !== 'object') + { + const msg = `the route headers must be of type object, found: ${typeof route.headers}, in route: ${name}` + throw new InvalidRouteError(msg) + } + + // validate that all headers match + const isHeadersValid = Object.keys(route.headers).every((header) => + { + const headerRegExp = new RegExp(`^${route.headers[header]}$`, 'i') + + if(header in request.headers + && request.headers[header].match(headerRegExp)) + { + return true + } + }) + + if(isHeadersValid) + { + validRoutes.push(route) + + // when an endpoint has been found, the route is terminated + if(route.endpoint) + { + return validRoutes + } + } + } + else + { + validRoutes.push(route) + + // when an endpoint has been found, the route is terminated + if(route.endpoint) + { + return validRoutes + } + } + } + } + + return validRoutes + } + + /** + * @private + * @param {string} segment + */ + mapSegments(segment) + { + if(segment.startsWith(':')) + { + if(segment.includes('=')) + { + return segment.split('=').pop() + } + else + { + return '[^/]+' + } + } + else + { + return segment + } + } + + /** + * @param {HttpServerRouteBuilderDtoBuilder} dtoBuilder + */ + addDtoBuilder(dtoBuilder) + { + if(typeof dtoBuilder.build !== 'function') + { + const msg = 'Expected "dtoBuilder" to have a build function' + throw new DtoBuilderContractNotHoneredError(msg) + } + + return this.dtoBuilders.push(dtoBuilder) + } + + /** + * @param {number} index + */ + removeDtoBuilder(index) + { + return delete this.dtoBuilders[index - 1] + } + + /** + * @param {Object} request + * @param {Object} route + */ + composeDto(request, route) + { + const dto = {} + + for(const index in this.dtoBuilders) + { + const dtoBuilder = this.dtoBuilders[index] + dtoBuilder.build(dto, route, request) + } + + return this.composer.compose(route.input, dto) + } +} + +module.exports = HttpServerRouteBuilder diff --git a/core/http/server/route/builder/locator.js b/core/http/server/route/builder/locator.js index 75cdfb8..31fee94 100644 --- a/core/http/server/route/builder/locator.js +++ b/core/http/server/route/builder/locator.js @@ -1,22 +1,22 @@ -const HttpServerRouteBuilder = require('.') - -class HttpServerRouteBuilderLocator -{ - constructor(locator) - { - this.locator = locator - } - - locate() - { - const - deepmerge = this.locator.locate('core/deepmerge'), - deepclone = this.locator.locate('core/deepclone'), - composer = this.locator.locate('core/schema/composer'), - builder = new HttpServerRouteBuilder(deepmerge, deepclone, composer) - - return builder - } -} - -module.exports = HttpServerRouteBuilderLocator +const HttpServerRouteBuilder = require('.') + +class HttpServerRouteBuilderLocator +{ + constructor(locator) + { + this.locator = locator + } + + locate() + { + const + deepmerge = this.locator.locate('core/deepmerge'), + deepclone = this.locator.locate('core/deepclone'), + composer = this.locator.locate('core/schema/composer'), + builder = new HttpServerRouteBuilder(deepmerge, deepclone, composer) + + return builder + } +} + +module.exports = HttpServerRouteBuilderLocator diff --git a/core/http/server/session/builder/index.js b/core/http/server/session/builder/index.js index a182d78..5643618 100644 --- a/core/http/server/session/builder/index.js +++ b/core/http/server/session/builder/index.js @@ -1,34 +1,34 @@ -const Cookies = require('cookies') - -class SessionBuilder -{ - build(request, response, domain) - { - let cookies - - const session = - { - domain, - - request, - - response, - - set cookies(value) - { - throw new Error('"cookies" can not be set on session, protected member') - }, - - get cookies() - { - return cookies - ? cookies - : cookies = new Cookies(request, response) - } - } - - return session - } -} - -module.exports = SessionBuilder +const Cookies = require('cookies') + +class SessionBuilder +{ + build(request, response, domain) + { + let cookies + + const session = + { + domain, + + request, + + response, + + set cookies(value) + { + throw new Error('"cookies" can not be set on session, protected member') + }, + + get cookies() + { + return cookies + ? cookies + : cookies = new Cookies(request, response) + } + } + + return session + } +} + +module.exports = SessionBuilder diff --git a/core/http/server/session/builder/locator.js b/core/http/server/session/builder/locator.js index 5c7fd00..c9fe3f8 100644 --- a/core/http/server/session/builder/locator.js +++ b/core/http/server/session/builder/locator.js @@ -1,11 +1,11 @@ -const SessionBuilder = require('.') - -class SessionBuilderLocator -{ - locate() - { - return new SessionBuilder - } -} - -module.exports = SessionBuilderLocator +const SessionBuilder = require('.') + +class SessionBuilderLocator +{ + locate() + { + return new SessionBuilder + } +} + +module.exports = SessionBuilderLocator diff --git a/core/http/server/view/index.js b/core/http/server/view/index.js index 24fa4c0..c4e1ac7 100644 --- a/core/http/server/view/index.js +++ b/core/http/server/view/index.js @@ -1,12 +1,12 @@ -class HttpView -{ - write(output, viewModel) - { - viewModel.headers['content-length'] = Buffer.byteLength(viewModel.body) - - output.writeHead(viewModel.meta.status || 200, viewModel.headers) - output.end(viewModel.body) - } -} - -module.exports = HttpView +class HttpView +{ + write(output, viewModel) + { + viewModel.headers['content-length'] = Buffer.byteLength(viewModel.body) + + output.writeHead(viewModel.meta.status || 200, viewModel.headers) + output.end(viewModel.body) + } +} + +module.exports = HttpView diff --git a/core/http/server/view/json/index.js b/core/http/server/view/json/index.js index cb9dd87..6ce803e 100644 --- a/core/http/server/view/json/index.js +++ b/core/http/server/view/json/index.js @@ -1,17 +1,17 @@ -class HttpViewJson -{ - write(output, viewModel, route) - { - const body = viewModel.meta.pretty || route.pretty - ? JSON.stringify(viewModel.body, null, 2) - : JSON.stringify(viewModel.body) - - viewModel.headers['content-type'] = 'application/json; charset=utf-8' - viewModel.headers['content-length'] = Buffer.byteLength(body) - - output.writeHead(viewModel.meta.status || 200, viewModel.headers) - output.end(body) - } -} - -module.exports = HttpViewJson +class HttpViewJson +{ + write(output, viewModel, route) + { + const body = viewModel.meta.pretty || route.pretty + ? JSON.stringify(viewModel.body, null, 2) + : JSON.stringify(viewModel.body) + + viewModel.headers['content-type'] = 'application/json; charset=utf-8' + viewModel.headers['content-length'] = Buffer.byteLength(body) + + output.writeHead(viewModel.meta.status || 200, viewModel.headers) + output.end(body) + } +} + +module.exports = HttpViewJson diff --git a/core/http/server/view/json/locator.js b/core/http/server/view/json/locator.js index 524fa78..6c91a06 100644 --- a/core/http/server/view/json/locator.js +++ b/core/http/server/view/json/locator.js @@ -1,11 +1,11 @@ -const HttpViewJson = require('.') - -class HttpViewJsonLocator -{ - locate() - { - return new HttpViewJson - } -} - -module.exports = HttpViewJsonLocator +const HttpViewJson = require('.') + +class HttpViewJsonLocator +{ + locate() + { + return new HttpViewJson + } +} + +module.exports = HttpViewJsonLocator diff --git a/core/http/server/view/locator.js b/core/http/server/view/locator.js index 8532fd8..7c6864c 100644 --- a/core/http/server/view/locator.js +++ b/core/http/server/view/locator.js @@ -1,11 +1,11 @@ -const HttpView = require('.') - -class HttpViewLocator -{ - locate() - { - return new HttpView - } -} - -module.exports = HttpViewLocator +const HttpView = require('.') + +class HttpViewLocator +{ + locate() + { + return new HttpView + } +} + +module.exports = HttpViewLocator diff --git a/core/http/server/view/stream/index.js b/core/http/server/view/stream/index.js index de0647a..05dc025 100644 --- a/core/http/server/view/stream/index.js +++ b/core/http/server/view/stream/index.js @@ -1,10 +1,10 @@ -class HttpViewStream -{ - write(output, viewModel, route) - { - output.writeHead(viewModel.meta.status || 200, viewModel.headers) - viewModel.meta.stream.pipe(output) - } -} - -module.exports = HttpViewStream +class HttpViewStream +{ + write(output, viewModel, route) + { + output.writeHead(viewModel.meta.status || 200, viewModel.headers) + viewModel.meta.stream.pipe(output) + } +} + +module.exports = HttpViewStream diff --git a/core/http/server/view/stream/locator.js b/core/http/server/view/stream/locator.js index 46b85b9..9a63fac 100644 --- a/core/http/server/view/stream/locator.js +++ b/core/http/server/view/stream/locator.js @@ -1,11 +1,11 @@ -const HttpViewStream = require('.') - -class HttpViewStreamLocator -{ - locate() - { - return new HttpViewStream - } -} - -module.exports = HttpViewStreamLocator +const HttpViewStream = require('.') + +class HttpViewStreamLocator +{ + locate() + { + return new HttpViewStream + } +} + +module.exports = HttpViewStreamLocator diff --git a/core/http/server/view/text/index.js b/core/http/server/view/text/index.js index 4854a45..8cff5fe 100644 --- a/core/http/server/view/text/index.js +++ b/core/http/server/view/text/index.js @@ -1,15 +1,15 @@ -class HttpViewText -{ - write(output, viewModel) - { - const body = `${viewModel.body}` - - viewModel.headers['content-type'] = 'text/plain; charset=utf-8' - viewModel.headers['content-length'] = Buffer.byteLength(body) - - output.writeHead(viewModel.status || 200, viewModel.headers) - output.end(body) - } -} - -module.exports = HttpViewText +class HttpViewText +{ + write(output, viewModel) + { + const body = `${viewModel.body}` + + viewModel.headers['content-type'] = 'text/plain; charset=utf-8' + viewModel.headers['content-length'] = Buffer.byteLength(body) + + output.writeHead(viewModel.status || 200, viewModel.headers) + output.end(body) + } +} + +module.exports = HttpViewText diff --git a/core/http/server/view/text/locator.js b/core/http/server/view/text/locator.js index c29c49e..d7a3d6a 100644 --- a/core/http/server/view/text/locator.js +++ b/core/http/server/view/text/locator.js @@ -1,11 +1,11 @@ -const HttpViewText = require('.') - -class HttpViewTextLocator -{ - locate() - { - return new HttpViewText - } -} - -module.exports = HttpViewTextLocator +const HttpViewText = require('.') + +class HttpViewTextLocator +{ + locate() + { + return new HttpViewText + } +} + +module.exports = HttpViewTextLocator diff --git a/core/locator/constituent.js b/core/locator/constituent.js index 84ed33c..311ac1a 100644 --- a/core/locator/constituent.js +++ b/core/locator/constituent.js @@ -1,26 +1,26 @@ -const LocatorNotImplementedError = require('./error/locator-not-implemented') - -/** - * For classes that represent a locator constituent of a composite pattern. - * - * @abstract - */ -class LocatorConstituent -{ - constructor(locator) - { - this.locator = locator - } - - /** - * A factory method for a service - * @returns {*} An instenace of the service that is being located - * @abstract - */ - locate() - { - throw new LocatorNotImplementedError('the "locate" method was not overwritten') - } -} - -module.exports = LocatorConstituent +const LocatorNotImplementedError = require('./error/locator-not-implemented') + +/** + * For classes that represent a locator constituent of a composite pattern. + * + * @abstract + */ +class LocatorConstituent +{ + constructor(locator) + { + this.locator = locator + } + + /** + * A factory method for a service + * @returns {*} An instenace of the service that is being located + * @abstract + */ + locate() + { + throw new LocatorNotImplementedError('the "locate" method was not overwritten') + } +} + +module.exports = LocatorConstituent diff --git a/core/locator/error/locator-not-implemented.js b/core/locator/error/locator-not-implemented.js index f86d0b4..e815c30 100644 --- a/core/locator/error/locator-not-implemented.js +++ b/core/locator/error/locator-not-implemented.js @@ -1,10 +1,10 @@ -class LocatorNotImplementedError extends Error -{ - constructor(...a) - { - super(...a) - this.code = 'E_LOCATOR_NOT_IMPLEMENTED' - } -} - -module.exports = LocatorNotImplementedError +class LocatorNotImplementedError extends Error +{ + constructor(...a) + { + super(...a) + this.code = 'E_LOCATOR_NOT_IMPLEMENTED' + } +} + +module.exports = LocatorNotImplementedError diff --git a/core/locator/error/service-undefined.js b/core/locator/error/service-undefined.js index 2d95327..14c9c1d 100644 --- a/core/locator/error/service-undefined.js +++ b/core/locator/error/service-undefined.js @@ -1,10 +1,10 @@ -class ServiceUndefinedError extends ReferenceError -{ - constructor(...a) - { - super(...a) - this.code = 'E_SERVICE_UNDEFINED' - } -} - -module.exports = ServiceUndefinedError +class ServiceUndefinedError extends ReferenceError +{ + constructor(...a) + { + super(...a) + this.code = 'E_SERVICE_UNDEFINED' + } +} + +module.exports = ServiceUndefinedError diff --git a/core/locator/index.js b/core/locator/index.js index d093a4e..d4261f4 100644 --- a/core/locator/index.js +++ b/core/locator/index.js @@ -1,29 +1,29 @@ -const ServiceUndefinedError = require('./error/service-undefined') - -class Locator -{ - constructor() - { - this.services = {} - } - - set(name, service) - { - this.services[name] = service - } - - has(name) - { - return name in this.services - } - - locate(service) - { - if(service in this.services) - return this.services[service] - - throw new ServiceUndefinedError(`"${service}" can not be located`) - } -} - -module.exports = Locator +const ServiceUndefinedError = require('./error/service-undefined') + +class Locator +{ + constructor() + { + this.services = {} + } + + set(name, service) + { + this.services[name] = service + } + + has(name) + { + return name in this.services + } + + locate(service) + { + if(service in this.services) + return this.services[service] + + throw new ServiceUndefinedError(`"${service}" can not be located`) + } +} + +module.exports = Locator diff --git a/core/object/config.js b/core/object/config.js index 213940f..5139f24 100644 --- a/core/object/config.js +++ b/core/object/config.js @@ -1,10 +1,10 @@ -module.exports = -{ - core: - { - locator: - { - 'core/object' : __dirname - } - } -} +module.exports = +{ + core: + { + locator: + { + 'core/object' : __dirname + } + } +} diff --git a/core/object/index.js b/core/object/index.js index ba873dc..6bef4e5 100644 --- a/core/object/index.js +++ b/core/object/index.js @@ -1,42 +1,42 @@ -class CoreObject -{ - /** - * @example { FooBar:'FooBar' } => { foobar:'FooBar' } - * @param {object} o input to be manipulated - * @returns {object} - */ - composeLowerCaseKeyedObject(o) - { - const - object = o || {}, - objectKeys = Object.keys(object), - composed = objectKeys.reduce((c, k) => (c[k.toLowerCase()] = object[k], c), {}) - - return composed - } - - /** - * Creates a copy of an object excluding some keys. - * References are kept, so modifing Objects or arrays on the resulting object will modify the source one - * To avoid this behaivour clone the input before using or clone the output after - * @param {object} o source object to create a copy - * @param {...string} keys Keys to remove - * @returns Copy of object without the specified keys - * @author Lleonard Subirana (arsu.leo@gmail.com) - */ - composeObjectWithoutKeys(o, ...keys) - { - const - object = o || {}, - result = { ...object } - - for(const key of keys) - { - delete result[key] - } - - return result - } -} - -module.exports = CoreObject +class CoreObject +{ + /** + * @example { FooBar:'FooBar' } => { foobar:'FooBar' } + * @param {object} o input to be manipulated + * @returns {object} + */ + composeLowerCaseKeyedObject(o) + { + const + object = o || {}, + objectKeys = Object.keys(object), + composed = objectKeys.reduce((c, k) => (c[k.toLowerCase()] = object[k], c), {}) + + return composed + } + + /** + * Creates a copy of an object excluding some keys. + * References are kept, so modifing Objects or arrays on the resulting object will modify the source one + * To avoid this behaivour clone the input before using or clone the output after + * @param {object} o source object to create a copy + * @param {...string} keys Keys to remove + * @returns Copy of object without the specified keys + * @author Lleonard Subirana (arsu.leo@gmail.com) + */ + composeObjectWithoutKeys(o, ...keys) + { + const + object = o || {}, + result = { ...object } + + for(const key of keys) + { + delete result[key] + } + + return result + } +} + +module.exports = CoreObject diff --git a/core/object/locator.js b/core/object/locator.js index cdabbfb..c8bbec5 100644 --- a/core/object/locator.js +++ b/core/object/locator.js @@ -1,11 +1,11 @@ -const CoreObject = require('.') - -class CoreObjectLocator -{ - locate() - { - return new CoreObject - } -} - -module.exports = CoreObjectLocator +const CoreObject = require('.') + +class CoreObjectLocator +{ + locate() + { + return new CoreObject + } +} + +module.exports = CoreObjectLocator diff --git a/core/path/config.js b/core/path/config.js index 816869a..d2094e7 100644 --- a/core/path/config.js +++ b/core/path/config.js @@ -1,10 +1,10 @@ -module.exports = -{ - core: - { - locator: - { - 'core/path' : __dirname - } - } -} +module.exports = +{ + core: + { + locator: + { + 'core/path' : __dirname + } + } +} diff --git a/core/path/index.js b/core/path/index.js index 3dfd644..c8488ed 100644 --- a/core/path/index.js +++ b/core/path/index.js @@ -1,63 +1,63 @@ -const path = require('path') - -class Path -{ - constructor() - { - const - filename = require.main.filename, - dirname = this.dirname(filename) - - this.main = { filename, dirname } - } - - /** - * @see require.resolve - */ - isResolvable(filename) - { - try - { - require.resolve(filename) - return true - } - catch (error) - { - return false - } - } - - /** - * @see path.dirname - */ - dirname(filename) - { - return path.dirname(filename) - } - - /** - * @see path.normalize - */ - normalize(filename) - { - return path.normalize(filename) - } - - /** - * @see path.extname - */ - extension(filename) - { - return path.extname(filename) - } - - /** - * @see path.isAbsolute - */ - isAbsolute(filename) - { - return path.isAbsolute(filename) - } -} - -module.exports = Path +const path = require('path') + +class Path +{ + constructor() + { + const + filename = require.main.filename, + dirname = this.dirname(filename) + + this.main = { filename, dirname } + } + + /** + * @see require.resolve + */ + isResolvable(filename) + { + try + { + require.resolve(filename) + return true + } + catch (error) + { + return false + } + } + + /** + * @see path.dirname + */ + dirname(filename) + { + return path.dirname(filename) + } + + /** + * @see path.normalize + */ + normalize(filename) + { + return path.normalize(filename) + } + + /** + * @see path.extname + */ + extension(filename) + { + return path.extname(filename) + } + + /** + * @see path.isAbsolute + */ + isAbsolute(filename) + { + return path.isAbsolute(filename) + } +} + +module.exports = Path diff --git a/core/path/locator.js b/core/path/locator.js index b014a87..474f4da 100644 --- a/core/path/locator.js +++ b/core/path/locator.js @@ -1,11 +1,11 @@ -const Path = require('.') - -class PathLocator -{ - locate() - { - return new Path - } -} - -module.exports = PathLocator +const Path = require('.') + +class PathLocator +{ + locate() + { + return new Path + } +} + +module.exports = PathLocator diff --git a/core/process/bootstrap/index.js b/core/process/bootstrap/index.js index fa76801..a5f9519 100644 --- a/core/process/bootstrap/index.js +++ b/core/process/bootstrap/index.js @@ -1,31 +1,31 @@ -class EventBusBootstrap -{ - constructor(eventbus) - { - this.eventbus = eventbus - } - - bootstrap() - { - process.on('unhandledRejection', this.onError.bind(this)) - process.on('uncaughtException', this.onError.bind(this)) - } - - onError(error, rejectedPromise) - { - if(rejectedPromise && rejectedPromise.domain) - { - rejectedPromise.domain.emit('error', error) - } - else if(process.domain) - { - process.domain.emit('error', error) - } - else - { - this.eventbus.emit('core.error', error) - } - } -} - -module.exports = EventBusBootstrap +class EventBusBootstrap +{ + constructor(eventbus) + { + this.eventbus = eventbus + } + + bootstrap() + { + process.on('unhandledRejection', this.onError.bind(this)) + process.on('uncaughtException', this.onError.bind(this)) + } + + onError(error, rejectedPromise) + { + if(rejectedPromise && rejectedPromise.domain) + { + rejectedPromise.domain.emit('error', error) + } + else if(process.domain) + { + process.domain.emit('error', error) + } + else + { + this.eventbus.emit('core.error', error) + } + } +} + +module.exports = EventBusBootstrap diff --git a/core/process/bootstrap/locator.js b/core/process/bootstrap/locator.js index 837ecfa..7c53b12 100644 --- a/core/process/bootstrap/locator.js +++ b/core/process/bootstrap/locator.js @@ -1,17 +1,17 @@ -const ProcessBootstrap = require('.') - -class ProcessBootstrapLocator -{ - constructor(locator) - { - this.locator = locator - } - - locate() - { - const eventbus = this.locator.locate('core/eventbus') - return new ProcessBootstrap(eventbus) - } -} - -module.exports = ProcessBootstrapLocator +const ProcessBootstrap = require('.') + +class ProcessBootstrapLocator +{ + constructor(locator) + { + this.locator = locator + } + + locate() + { + const eventbus = this.locator.locate('core/eventbus') + return new ProcessBootstrap(eventbus) + } +} + +module.exports = ProcessBootstrapLocator diff --git a/core/process/config.js b/core/process/config.js index aa2e018..2aa109e 100644 --- a/core/process/config.js +++ b/core/process/config.js @@ -1,15 +1,15 @@ -module.exports = -{ - core: - { - bootstrap: - { - 'process' : 'core/process/bootstrap' - }, - locator: - { - 'core/process' : __dirname, - 'core/process/bootstrap' : __dirname + '/bootstrap' - } - } -} +module.exports = +{ + core: + { + bootstrap: + { + 'process' : 'core/process/bootstrap' + }, + locator: + { + 'core/process' : __dirname, + 'core/process/bootstrap' : __dirname + '/bootstrap' + } + } +} diff --git a/core/process/index.js b/core/process/index.js index 20dabf2..a60ec3d 100644 --- a/core/process/index.js +++ b/core/process/index.js @@ -1,9 +1,9 @@ -class Process -{ - exit(...a) - { - process.exit(...a) - } -} - -module.exports = Process +class Process +{ + exit(...a) + { + process.exit(...a) + } +} + +module.exports = Process diff --git a/core/process/locator.js b/core/process/locator.js index 2b974d5..2f3d28e 100644 --- a/core/process/locator.js +++ b/core/process/locator.js @@ -1,11 +1,11 @@ -const Process = require('.') - -class ProcessLocator -{ - locate() - { - return new Process - } -} - -module.exports = ProcessLocator +const Process = require('.') + +class ProcessLocator +{ + locate() + { + return new Process + } +} + +module.exports = ProcessLocator diff --git a/core/schema/bootstrap/error/schema-not-resolvable.js b/core/schema/bootstrap/error/schema-not-resolvable.js index 1832227..31d8630 100644 --- a/core/schema/bootstrap/error/schema-not-resolvable.js +++ b/core/schema/bootstrap/error/schema-not-resolvable.js @@ -1,10 +1,10 @@ -class SchemaNotResolvableError extends ReferenceError -{ - constructor(...a) - { - super(...a) - this.code = 'E_SCHEMA_NOT_RESOLVABLE' - } -} - -module.exports = SchemaNotResolvableError +class SchemaNotResolvableError extends ReferenceError +{ + constructor(...a) + { + super(...a) + this.code = 'E_SCHEMA_NOT_RESOLVABLE' + } +} + +module.exports = SchemaNotResolvableError diff --git a/core/schema/bootstrap/index.js b/core/schema/bootstrap/index.js index 71e5942..327a316 100644 --- a/core/schema/bootstrap/index.js +++ b/core/schema/bootstrap/index.js @@ -1,89 +1,89 @@ -const -fs = require('fs'), -SchemaNotResolvable = require('./error/schema-not-resolvable') - -class SchemaBootstrap -{ - constructor(locator, configuration, path) - { - this.locator = locator - this.configuration = configuration - this.path = path - } - - bootstrap() - { - const - composer = this.locator.locate('core/schema/composer'), - schemas = this.configuration.find('core.schema.composer'), - filters = this.configuration.find('core.schema.filter'), - validators = this.configuration.find('core.schema.validator') - - this.addSchemas(composer, schemas) - this.addFilters(composer, filters) - this.addValidators(composer, validators) - } - - addSchemas(composer, schemas) - { - for(const schemaName in schemas || []) - { - - if(schemaName.endsWith('/*')) - { - const - directoryPath = schemas[schemaName].slice(0, -1), - dirents = fs.readdirSync(directoryPath, { withFileTypes:true }) - - for(const dirent of dirents) - { - if(dirent.isFile() - && dirent.name.endsWith('.js')) - { - const - schemaNamePath = schemaName.slice(0, -1), - filename = dirent.name.slice(0, -3), - schemaFilepath = directoryPath + filename, - schemaNameMapped = schemaNamePath + filename, - schema = require(schemaFilepath) - - composer.addSchema(schemaNameMapped, schema) - } - } - } - else - { - if(this.path.isResolvable(schemas[schemaName])) - { - const schema = require(schemas[schemaName]) - composer.addSchema(schemaName, schema) - } - else - { - const msg = `Could not resolve path for schema: "${schemaName}", path: "${schemas[schemaName]}"` - throw new SchemaNotResolvable(msg) - } - } - } - } - - addFilters(composer, filters) - { - for(const filterName in filters || []) - { - const filter = this.locator.locate(filters[filterName]) - composer.addFilter(filterName, filter) - } - } - - addValidators(composer, validators) - { - for(const validatorName in validators || []) - { - const validator = this.locator.locate(validators[validatorName]) - composer.addValidator(validatorName, validator) - } - } -} - -module.exports = SchemaBootstrap +const +fs = require('fs'), +SchemaNotResolvable = require('./error/schema-not-resolvable') + +class SchemaBootstrap +{ + constructor(locator, configuration, path) + { + this.locator = locator + this.configuration = configuration + this.path = path + } + + bootstrap() + { + const + composer = this.locator.locate('core/schema/composer'), + schemas = this.configuration.find('core.schema.composer'), + filters = this.configuration.find('core.schema.filter'), + validators = this.configuration.find('core.schema.validator') + + this.addSchemas(composer, schemas) + this.addFilters(composer, filters) + this.addValidators(composer, validators) + } + + addSchemas(composer, schemas) + { + for(const schemaName in schemas || []) + { + + if(schemaName.endsWith('/*')) + { + const + directoryPath = schemas[schemaName].slice(0, -1), + dirents = fs.readdirSync(directoryPath, { withFileTypes:true }) + + for(const dirent of dirents) + { + if(dirent.isFile() + && dirent.name.endsWith('.js')) + { + const + schemaNamePath = schemaName.slice(0, -1), + filename = dirent.name.slice(0, -3), + schemaFilepath = directoryPath + filename, + schemaNameMapped = schemaNamePath + filename, + schema = require(schemaFilepath) + + composer.addSchema(schemaNameMapped, schema) + } + } + } + else + { + if(this.path.isResolvable(schemas[schemaName])) + { + const schema = require(schemas[schemaName]) + composer.addSchema(schemaName, schema) + } + else + { + const msg = `Could not resolve path for schema: "${schemaName}", path: "${schemas[schemaName]}"` + throw new SchemaNotResolvable(msg) + } + } + } + } + + addFilters(composer, filters) + { + for(const filterName in filters || []) + { + const filter = this.locator.locate(filters[filterName]) + composer.addFilter(filterName, filter) + } + } + + addValidators(composer, validators) + { + for(const validatorName in validators || []) + { + const validator = this.locator.locate(validators[validatorName]) + composer.addValidator(validatorName, validator) + } + } +} + +module.exports = SchemaBootstrap diff --git a/core/schema/bootstrap/locator.js b/core/schema/bootstrap/locator.js index 4e2d693..d6f7c24 100644 --- a/core/schema/bootstrap/locator.js +++ b/core/schema/bootstrap/locator.js @@ -1,20 +1,20 @@ -const SchemaBootstrap = require('.') - -class SchemaBootstrapLocator -{ - constructor(locator) - { - this.locator = locator - } - - locate() - { - const - configuration = this.locator.locate('core/configuration'), - path = this.locator.locate('core/path') - - return new SchemaBootstrap(this.locator, configuration, path) - } -} - -module.exports = SchemaBootstrapLocator +const SchemaBootstrap = require('.') + +class SchemaBootstrapLocator +{ + constructor(locator) + { + this.locator = locator + } + + locate() + { + const + configuration = this.locator.locate('core/configuration'), + path = this.locator.locate('core/path') + + return new SchemaBootstrap(this.locator, configuration, path) + } +} + +module.exports = SchemaBootstrapLocator diff --git a/core/schema/composer/error/filter-is-not-honering-contract.js b/core/schema/composer/error/filter-is-not-honering-contract.js index 286d12d..a3c8d1e 100644 --- a/core/schema/composer/error/filter-is-not-honering-contract.js +++ b/core/schema/composer/error/filter-is-not-honering-contract.js @@ -1,13 +1,13 @@ -/** - * @extends {Error} - */ -class FilterIsNotHoneringContractError extends Error -{ - constructor(...a) - { - super(...a) - this.code = 'E_FILTER_IS_NOT_HONERING_CONTRACT' - } -} - -module.exports = FilterIsNotHoneringContractError +/** + * @extends {Error} + */ +class FilterIsNotHoneringContractError extends Error +{ + constructor(...a) + { + super(...a) + this.code = 'E_FILTER_IS_NOT_HONERING_CONTRACT' + } +} + +module.exports = FilterIsNotHoneringContractError diff --git a/core/schema/composer/error/invalid-attribute.js b/core/schema/composer/error/invalid-attribute.js index ad9a78e..fc62e49 100644 --- a/core/schema/composer/error/invalid-attribute.js +++ b/core/schema/composer/error/invalid-attribute.js @@ -1,13 +1,13 @@ -/** - * @extends {Error} - */ -class InvalidAttributeError extends Error -{ - constructor(...a) - { - super(...a) - this.code = 'E_SCHEMA_INVALID_ATTRIBUTE' - } -} - -module.exports = InvalidAttributeError +/** + * @extends {Error} + */ +class InvalidAttributeError extends Error +{ + constructor(...a) + { + super(...a) + this.code = 'E_SCHEMA_INVALID_ATTRIBUTE' + } +} + +module.exports = InvalidAttributeError diff --git a/core/schema/composer/error/invalid-collection.js b/core/schema/composer/error/invalid-collection.js index f0bc767..522b0f3 100644 --- a/core/schema/composer/error/invalid-collection.js +++ b/core/schema/composer/error/invalid-collection.js @@ -1,13 +1,13 @@ -/** - * @extends {Error} - */ -class InvalidCollectionError extends Error -{ - constructor(...a) - { - super(...a) - this.code = 'E_INVALID_COLLECTION' - } -} - -module.exports = InvalidCollectionError +/** + * @extends {Error} + */ +class InvalidCollectionError extends Error +{ + constructor(...a) + { + super(...a) + this.code = 'E_INVALID_COLLECTION' + } +} + +module.exports = InvalidCollectionError diff --git a/core/schema/composer/error/invalid-schema.js b/core/schema/composer/error/invalid-schema.js index 6f178e7..9ad8f19 100644 --- a/core/schema/composer/error/invalid-schema.js +++ b/core/schema/composer/error/invalid-schema.js @@ -1,13 +1,13 @@ -/** - * @extends {Error} - */ -class InvalidSchemaError extends Error -{ - constructor(...a) - { - super(...a) - this.code = 'E_SCHEMA_INVALID_SCHEMA' - } -} - -module.exports = InvalidSchemaError +/** + * @extends {Error} + */ +class InvalidSchemaError extends Error +{ + constructor(...a) + { + super(...a) + this.code = 'E_SCHEMA_INVALID_SCHEMA' + } +} + +module.exports = InvalidSchemaError diff --git a/core/schema/composer/error/schema-not-found.js b/core/schema/composer/error/schema-not-found.js index 4844d5e..1bdbacd 100644 --- a/core/schema/composer/error/schema-not-found.js +++ b/core/schema/composer/error/schema-not-found.js @@ -1,13 +1,13 @@ -/** - * @extends {Error} - */ -class SchemaNotFoundError extends Error -{ - constructor(...a) - { - super(...a) - this.code = 'E_SCHEMA_NOT_FOUND' - } -} - -module.exports = SchemaNotFoundError +/** + * @extends {Error} + */ +class SchemaNotFoundError extends Error +{ + constructor(...a) + { + super(...a) + this.code = 'E_SCHEMA_NOT_FOUND' + } +} + +module.exports = SchemaNotFoundError diff --git a/core/schema/composer/error/validator-is-not-honering-contract.js b/core/schema/composer/error/validator-is-not-honering-contract.js index 685cecc..1e14505 100644 --- a/core/schema/composer/error/validator-is-not-honering-contract.js +++ b/core/schema/composer/error/validator-is-not-honering-contract.js @@ -1,13 +1,13 @@ -/** - * @extends {Error} - */ -class ValidatorIsNotHoneringContractError extends Error -{ - constructor(...a) - { - super(...a) - this.code = 'E_VALIDATOR_IS_NOT_HONERING_CONTRACT' - } -} - -module.exports = ValidatorIsNotHoneringContractError +/** + * @extends {Error} + */ +class ValidatorIsNotHoneringContractError extends Error +{ + constructor(...a) + { + super(...a) + this.code = 'E_VALIDATOR_IS_NOT_HONERING_CONTRACT' + } +} + +module.exports = ValidatorIsNotHoneringContractError diff --git a/core/schema/composer/error/validator-not-found.js b/core/schema/composer/error/validator-not-found.js index a558ccc..f4f0062 100644 --- a/core/schema/composer/error/validator-not-found.js +++ b/core/schema/composer/error/validator-not-found.js @@ -1,13 +1,13 @@ -/** - * @extends {Error} - */ -class ValidatorNotFoundError extends Error -{ - constructor(...a) - { - super(...a) - this.code = 'E_VALIDATOR_NOT_FOUND' - } -} - -module.exports = ValidatorNotFoundError +/** + * @extends {Error} + */ +class ValidatorNotFoundError extends Error +{ + constructor(...a) + { + super(...a) + this.code = 'E_VALIDATOR_NOT_FOUND' + } +} + +module.exports = ValidatorNotFoundError diff --git a/core/schema/composer/index.js b/core/schema/composer/index.js index 574e521..226e5c0 100644 --- a/core/schema/composer/index.js +++ b/core/schema/composer/index.js @@ -1,353 +1,353 @@ -const -InvalidAttributeError = require('./error/invalid-attribute'), -InvalidCollectionError = require('./error/invalid-collection'), -InvalidSchemaError = require('./error/invalid-schema'), -SchemaNotFoundError = require('./error/schema-not-found'), -FilterIsNotHoneringContractError = require('./error/filter-is-not-honering-contract'), -ValidatorIsNotHoneringContractError = require('./error/validator-is-not-honering-contract'), -ValidatorNotFoundError = require('./error/validator-not-found') - -class SchemaComposer -{ - constructor(deepmerge, deepclone, deepfreeze) - { - this.deepmerge = deepmerge - this.deepclone = deepclone - this.deepfreeze = deepfreeze - this.schemas = {} - this.filters = {} - this.validators = {} - } - - /** - * @param {string} schemaName - * @param {Object|Array} dto - * - * @throws {E_SCHEMA_NOT_FOUND} - * @throws {E_VALIDATOR_NOT_FOUND} - * @throws {E_SCHEMA_INVALID_ATTRIBUTE} - * - * @returns {Object} - */ - compose(schemaName, dto) - { - const - schema = this.composeSchema(schemaName), - output = {} - - if(Array.isArray(dto)) - { - dto = this.deepmerge.merge({}, ...dto) - } - - for(const attribute in schema) - { - output[attribute] = this.attribute(schemaName, schema, attribute, dto[attribute]) - } - - if(Object.isFrozen(schema)) - { - this.deepfreeze.freeze(output) - } - - return output - } - - /** - * @param {string} schemaName - * @param {boolean} includeOptional - * - * @throws {E_SCHEMA_NOT_FOUND} - * - * @returns {Object} - */ - composeExample(schemaName, includeOptional) - { - const - schema = this.composeSchema(schemaName), - output = {} - - for(const attribute in schema) - { - if(schema[attribute].optional) - { - if(includeOptional) - { - output[attribute] = this.composeExampleValue(schema[attribute], includeOptional) - } - } - else - { - output[attribute] = this.composeExampleValue(schema[attribute], includeOptional) - } - } - - if(Object.isFrozen(schema)) - { - this.deepfreeze.freeze(output) - } - - return output - } - - composeExampleValue(options, includeOptional) - { - let output - - if('example' in options) - { - output = options.example - } - else if(typeof options.schema === 'string') - { - const example = this.composeExample(options.schema, includeOptional) - - output = options.trait - ? example[options.trait] - : example - } - - return options.collection - ? output ? [output] : [] - : output - } - - /** - * @param {string} schemaName - * - * @throws {E_SCHEMA_NOT_FOUND} - * - * @returns {Object} - */ - composeSchema(schemaName) - { - if(schemaName in this.schemas === false) - { - const msg = `Schema: "${schemaName}" not found` - throw new SchemaNotFoundError(msg) - } - - const schema = this.buildSchema(this.schemas[schemaName]) - - return schema - } - - /** - * @param {string} schemaName - * @param {string} attribute - * @param {Object} data - * - * @throws {E_SCHEMA_NOT_FOUND} - * @throws {E_VALIDATOR_NOT_FOUND} - * @throws {E_SCHEMA_INVALID_ATTRIBUTE} - * - * @returns {*} - */ - trait(schemaName, attribute, data) - { - if(schemaName in this.schemas === false) - { - const msg = `Schema: "${schemaName}" not found` - throw new SchemaNotFoundError(msg) - } - - const - schema = this.schemas[schemaName], - output = this.attribute(schemaName, schema, attribute, data) - - return output - } - - /** - * @private - */ - attribute(schemaName, schema, attribute, data) - { - const options = schema[attribute] - - if('default' in options && data === undefined) - { - data = options.default - } - - // if optional, and undefined or null, then we don't need to filter or validate - if(options.optional === true && data === undefined) - { - return data - } - - if(options.nullable === true && data === null) - { - return data - } - - // Filtering attributes if a filter has been defined for the type - if(options.type in this.filters) - { - const filter = this.filters[options.type] - data = filter.filter(options, data) - } - - // Validating type - if(options.type in this.validators === false) - { - const msg = `In schema: "${schemaName}", validator: "${options.type}" not found` - throw new ValidatorNotFoundError(msg) - } - - try - { - const validator = this.validators[options.type] - - if(options.collection) - { - if(!Array.isArray(data)) - { - const msg = `In schema: "${schemaName}", ` - + `invalid type: "${typeof data}", ` - + `array expected` - - throw new InvalidCollectionError(msg) - } - - if(options['collection-size-min'] - && options['collection-size-min'] > data.length) - { - const msg = `In schema: "${schemaName}", ` - + `invalid collection size, expected min ${options['collection-size-min']}, ` - + `received size ${data.length}` - - throw new InvalidCollectionError(msg) - } - - if(options['collection-size-max'] - && options['collection-size-max'] < data.length) - { - const msg = `In schema: "${schemaName}", ` - + `invalid collection size, expected max ${options['collection-size-max']}, ` - + `received size ${data.length}` - - throw new InvalidCollectionError(msg) - } - - for(const item of data) - { - validator.valid(options, item) - } - } - else - { - validator.valid(options, data) - } - } - catch(error) - { - const msg = `Invalid attribute: "${attribute}", schema: "${schemaName}", error: ${error.message}` - throw new InvalidAttributeError(msg) - } - - return data - } - - /** - * The schema could have declared a meta field that requires a building process before used - * The build process will alter the schema provided through an argument - * - * @param {Object} schema - * @return {Object} Same instance as provided through argument - */ - buildSchema(schema) - { - if('@meta' in schema) - { - if('extends' in schema['@meta'] - || 'extend' in schema['@meta']) - { - const extendList = schema['@meta'].extends - || schema['@meta'].extend - - for(const extendSchemaName of Array.isArray(extendList) ? extendList : [extendList]) - { - if(extendSchemaName in this.schemas) - { - const extend = this.buildSchema(this.schemas[extendSchemaName]) - this.deepmerge.merge(schema, extend) - } - else - { - const msg = `Schema "${extendSchemaName}" does not exist` - throw new InvalidSchemaError(msg) - } - } - } - - if(schema['@meta'].immutable - || schema['@meta'].immutable === undefined) - { - delete schema['@meta'] - Object.freeze(schema) - } - else - { - delete schema['@meta'] - } - } - - return schema - } - - /** - * @param {string} schemaName - * @param {Object} schema - * @throws {E_SCHEMA_INVALID_SCHEMA} - */ - addSchema(schemaName, schema) - { - if(typeof schema !== 'object') - { - const msg = `Schema "${schemaName}" must be an object` - throw new InvalidSchemaError(msg) - } - - // TODO: Improve validation of the schema when it's added - // For the moment we can suffer unexpected errors when we start working with the schema - // A better approch is to validate the schema structure as a value object - // ...resulting in a garantee that the schema is of expected definition. - - this.schemas[schemaName] = this.deepclone.clone(schema) - } - - /** - * @param {string} filterName - * @param {SchemaFilter} filter - * @throws {E_FILTER_IS_NOT_HONERING_CONTRACT} - */ - addFilter(filterName, filter) - { - if(typeof filter.filter !== 'function') - { - const msg = `Filter "${filterName}" not honering contract` - throw new FilterIsNotHoneringContractError(msg) - } - - this.filters[filterName] = filter - } - - /** - * @param {string} validatorName - * @param {SchemaValidator} validator - * @throws {E_VALIDATOR_IS_NOT_HONERING_CONTRACT} - */ - addValidator(validatorName, validator) - { - if(typeof validator.valid !== 'function') - { - const msg = `Validator "${validatorName}" not honering contract` - throw new ValidatorIsNotHoneringContractError(msg) - } - - this.validators[validatorName] = validator - } -} - -module.exports = SchemaComposer +const +InvalidAttributeError = require('./error/invalid-attribute'), +InvalidCollectionError = require('./error/invalid-collection'), +InvalidSchemaError = require('./error/invalid-schema'), +SchemaNotFoundError = require('./error/schema-not-found'), +FilterIsNotHoneringContractError = require('./error/filter-is-not-honering-contract'), +ValidatorIsNotHoneringContractError = require('./error/validator-is-not-honering-contract'), +ValidatorNotFoundError = require('./error/validator-not-found') + +class SchemaComposer +{ + constructor(deepmerge, deepclone, deepfreeze) + { + this.deepmerge = deepmerge + this.deepclone = deepclone + this.deepfreeze = deepfreeze + this.schemas = {} + this.filters = {} + this.validators = {} + } + + /** + * @param {string} schemaName + * @param {Object|Array} dto + * + * @throws {E_SCHEMA_NOT_FOUND} + * @throws {E_VALIDATOR_NOT_FOUND} + * @throws {E_SCHEMA_INVALID_ATTRIBUTE} + * + * @returns {Object} + */ + compose(schemaName, dto) + { + const + schema = this.composeSchema(schemaName), + output = {} + + if(Array.isArray(dto)) + { + dto = this.deepmerge.merge({}, ...dto) + } + + for(const attribute in schema) + { + output[attribute] = this.attribute(schemaName, schema, attribute, dto[attribute]) + } + + if(Object.isFrozen(schema)) + { + this.deepfreeze.freeze(output) + } + + return output + } + + /** + * @param {string} schemaName + * @param {boolean} includeOptional + * + * @throws {E_SCHEMA_NOT_FOUND} + * + * @returns {Object} + */ + composeExample(schemaName, includeOptional) + { + const + schema = this.composeSchema(schemaName), + output = {} + + for(const attribute in schema) + { + if(schema[attribute].optional) + { + if(includeOptional) + { + output[attribute] = this.composeExampleValue(schema[attribute], includeOptional) + } + } + else + { + output[attribute] = this.composeExampleValue(schema[attribute], includeOptional) + } + } + + if(Object.isFrozen(schema)) + { + this.deepfreeze.freeze(output) + } + + return output + } + + composeExampleValue(options, includeOptional) + { + let output + + if('example' in options) + { + output = options.example + } + else if(typeof options.schema === 'string') + { + const example = this.composeExample(options.schema, includeOptional) + + output = options.trait + ? example[options.trait] + : example + } + + return options.collection + ? output ? [output] : [] + : output + } + + /** + * @param {string} schemaName + * + * @throws {E_SCHEMA_NOT_FOUND} + * + * @returns {Object} + */ + composeSchema(schemaName) + { + if(schemaName in this.schemas === false) + { + const msg = `Schema: "${schemaName}" not found` + throw new SchemaNotFoundError(msg) + } + + const schema = this.buildSchema(this.schemas[schemaName]) + + return schema + } + + /** + * @param {string} schemaName + * @param {string} attribute + * @param {Object} data + * + * @throws {E_SCHEMA_NOT_FOUND} + * @throws {E_VALIDATOR_NOT_FOUND} + * @throws {E_SCHEMA_INVALID_ATTRIBUTE} + * + * @returns {*} + */ + trait(schemaName, attribute, data) + { + if(schemaName in this.schemas === false) + { + const msg = `Schema: "${schemaName}" not found` + throw new SchemaNotFoundError(msg) + } + + const + schema = this.schemas[schemaName], + output = this.attribute(schemaName, schema, attribute, data) + + return output + } + + /** + * @private + */ + attribute(schemaName, schema, attribute, data) + { + const options = schema[attribute] + + if('default' in options && data === undefined) + { + data = options.default + } + + // if optional, and undefined or null, then we don't need to filter or validate + if(options.optional === true && data === undefined) + { + return data + } + + if(options.nullable === true && data === null) + { + return data + } + + // Filtering attributes if a filter has been defined for the type + if(options.type in this.filters) + { + const filter = this.filters[options.type] + data = filter.filter(options, data) + } + + // Validating type + if(options.type in this.validators === false) + { + const msg = `In schema: "${schemaName}", validator: "${options.type}" not found` + throw new ValidatorNotFoundError(msg) + } + + try + { + const validator = this.validators[options.type] + + if(options.collection) + { + if(!Array.isArray(data)) + { + const msg = `In schema: "${schemaName}", ` + + `invalid type: "${typeof data}", ` + + `array expected` + + throw new InvalidCollectionError(msg) + } + + if(options['collection-size-min'] + && options['collection-size-min'] > data.length) + { + const msg = `In schema: "${schemaName}", ` + + `invalid collection size, expected min ${options['collection-size-min']}, ` + + `received size ${data.length}` + + throw new InvalidCollectionError(msg) + } + + if(options['collection-size-max'] + && options['collection-size-max'] < data.length) + { + const msg = `In schema: "${schemaName}", ` + + `invalid collection size, expected max ${options['collection-size-max']}, ` + + `received size ${data.length}` + + throw new InvalidCollectionError(msg) + } + + for(const item of data) + { + validator.valid(options, item) + } + } + else + { + validator.valid(options, data) + } + } + catch(error) + { + const msg = `Invalid attribute: "${attribute}", schema: "${schemaName}", error: ${error.message}` + throw new InvalidAttributeError(msg) + } + + return data + } + + /** + * The schema could have declared a meta field that requires a building process before used + * The build process will alter the schema provided through an argument + * + * @param {Object} schema + * @return {Object} Same instance as provided through argument + */ + buildSchema(schema) + { + if('@meta' in schema) + { + if('extends' in schema['@meta'] + || 'extend' in schema['@meta']) + { + const extendList = schema['@meta'].extends + || schema['@meta'].extend + + for(const extendSchemaName of Array.isArray(extendList) ? extendList : [extendList]) + { + if(extendSchemaName in this.schemas) + { + const extend = this.buildSchema(this.schemas[extendSchemaName]) + this.deepmerge.merge(schema, extend) + } + else + { + const msg = `Schema "${extendSchemaName}" does not exist` + throw new InvalidSchemaError(msg) + } + } + } + + if(schema['@meta'].immutable + || schema['@meta'].immutable === undefined) + { + delete schema['@meta'] + Object.freeze(schema) + } + else + { + delete schema['@meta'] + } + } + + return schema + } + + /** + * @param {string} schemaName + * @param {Object} schema + * @throws {E_SCHEMA_INVALID_SCHEMA} + */ + addSchema(schemaName, schema) + { + if(typeof schema !== 'object') + { + const msg = `Schema "${schemaName}" must be an object` + throw new InvalidSchemaError(msg) + } + + // TODO: Improve validation of the schema when it's added + // For the moment we can suffer unexpected errors when we start working with the schema + // A better approch is to validate the schema structure as a value object + // ...resulting in a garantee that the schema is of expected definition. + + this.schemas[schemaName] = this.deepclone.clone(schema) + } + + /** + * @param {string} filterName + * @param {SchemaFilter} filter + * @throws {E_FILTER_IS_NOT_HONERING_CONTRACT} + */ + addFilter(filterName, filter) + { + if(typeof filter.filter !== 'function') + { + const msg = `Filter "${filterName}" not honering contract` + throw new FilterIsNotHoneringContractError(msg) + } + + this.filters[filterName] = filter + } + + /** + * @param {string} validatorName + * @param {SchemaValidator} validator + * @throws {E_VALIDATOR_IS_NOT_HONERING_CONTRACT} + */ + addValidator(validatorName, validator) + { + if(typeof validator.valid !== 'function') + { + const msg = `Validator "${validatorName}" not honering contract` + throw new ValidatorIsNotHoneringContractError(msg) + } + + this.validators[validatorName] = validator + } +} + +module.exports = SchemaComposer diff --git a/core/schema/composer/locator.js b/core/schema/composer/locator.js index 294fb0f..cc0284a 100644 --- a/core/schema/composer/locator.js +++ b/core/schema/composer/locator.js @@ -1,21 +1,21 @@ -const Schema = require('.') - -class SchemaLocator -{ - constructor(locator) - { - this.locator = locator - } - - locate() - { - const - deepmerge = this.locator.locate('core/deepmerge'), - deepclone = this.locator.locate('core/deepclone'), - deepfreeze = this.locator.locate('core/deepfreeze') - - return new Schema(deepmerge, deepclone, deepfreeze) - } -} - -module.exports = SchemaLocator +const Schema = require('.') + +class SchemaLocator +{ + constructor(locator) + { + this.locator = locator + } + + locate() + { + const + deepmerge = this.locator.locate('core/deepmerge'), + deepclone = this.locator.locate('core/deepclone'), + deepfreeze = this.locator.locate('core/deepfreeze') + + return new Schema(deepmerge, deepclone, deepfreeze) + } +} + +module.exports = SchemaLocator diff --git a/core/schema/config.js b/core/schema/config.js index ba23a07..a35c27a 100644 --- a/core/schema/config.js +++ b/core/schema/config.js @@ -1,42 +1,44 @@ -module.exports = -{ - core: - { - bootstrap: - { - 'schema' : 'core/schema/bootstrap' - }, - schema: - { - filter: - { - 'boolean' : 'core/schema/filter/boolean', - 'csv' : 'core/schema/filter/csv', - 'decimal' : 'core/schema/filter/decimal', - 'integer' : 'core/schema/filter/integer', - 'json' : 'core/schema/filter/json', - 'schema' : 'core/schema/filter/schema', - 'string' : 'core/schema/filter/string', - 'timestamp' : 'core/schema/filter/timestamp' - }, - validator: - { - 'boolean' : 'core/schema/validator/boolean', - 'csv' : 'core/schema/validator/csv', - 'decimal' : 'core/schema/validator/decimal', - 'integer' : 'core/schema/validator/integer', - 'json' : 'core/schema/validator/json', - 'schema' : 'core/schema/validator/schema', - 'string' : 'core/schema/validator/string', - 'timestamp' : 'core/schema/validator/timestamp' - } - }, - locator: - { - 'core/schema/composer' : __dirname + '/composer', - 'core/schema/bootstrap' : __dirname + '/bootstrap', - 'core/schema/filter/*' : __dirname + '/filter/*', - 'core/schema/validator/*' : __dirname + '/validator/*', - } - } -} +module.exports = +{ + core: + { + bootstrap: + { + 'schema' : 'core/schema/bootstrap' + }, + schema: + { + filter: + { + 'boolean' : 'core/schema/filter/boolean', + 'csv' : 'core/schema/filter/csv', + 'decimal' : 'core/schema/filter/decimal', + 'integer' : 'core/schema/filter/integer', + 'json' : 'core/schema/filter/json', + 'schema' : 'core/schema/filter/schema', + 'string' : 'core/schema/filter/string', + 'timestamp' : 'core/schema/filter/timestamp', + 'phone-number' : 'core/schema/filter/phone-number' + }, + validator: + { + 'boolean' : 'core/schema/validator/boolean', + 'csv' : 'core/schema/validator/csv', + 'decimal' : 'core/schema/validator/decimal', + 'integer' : 'core/schema/validator/integer', + 'json' : 'core/schema/validator/json', + 'schema' : 'core/schema/validator/schema', + 'string' : 'core/schema/validator/string', + 'timestamp' : 'core/schema/validator/timestamp', + 'phone-number' : 'core/schema/validator/phone-number' + } + }, + locator: + { + 'core/schema/composer' : __dirname + '/composer', + 'core/schema/bootstrap' : __dirname + '/bootstrap', + 'core/schema/filter/*' : __dirname + '/filter/*', + 'core/schema/validator/*' : __dirname + '/validator/*', + } + } +} diff --git a/core/schema/filter/boolean/index.js b/core/schema/filter/boolean/index.js index 5172d2b..9394a6a 100644 --- a/core/schema/filter/boolean/index.js +++ b/core/schema/filter/boolean/index.js @@ -1,53 +1,53 @@ -/** - * @implements {SchemaFilter} - */ -class SchemaFilterBoolean -{ - filter(options, data) - { - return options.collection - ? this.filterCollection(data) - : this.filterSingle(data) - } - - filterCollection(data) - { - if(!Array.isArray(data)) - return data - - const collection = [] - - for(const item of data) - { - const filtered = this.filterSingle(item) - collection.push(filtered) - } - - return collection - } - - filterSingle(data) - { - if(typeof data === 'string') - { - if(data.toLowerCase() === 'true') - return true - - if(data.toLowerCase() === 'false') - return false - } - - if(typeof data === 'number') - { - if(data === 1) - return true - - if(data === 0) - return false - } - - return data - } -} - -module.exports = SchemaFilterBoolean +/** + * @implements {SchemaFilter} + */ +class SchemaFilterBoolean +{ + filter(options, data) + { + return options.collection + ? this.filterCollection(data) + : this.filterSingle(data) + } + + filterCollection(data) + { + if(!Array.isArray(data)) + return data + + const collection = [] + + for(const item of data) + { + const filtered = this.filterSingle(item) + collection.push(filtered) + } + + return collection + } + + filterSingle(data) + { + if(typeof data === 'string') + { + if(data.toLowerCase() === 'true') + return true + + if(data.toLowerCase() === 'false') + return false + } + + if(typeof data === 'number') + { + if(data === 1) + return true + + if(data === 0) + return false + } + + return data + } +} + +module.exports = SchemaFilterBoolean diff --git a/core/schema/filter/boolean/locator.js b/core/schema/filter/boolean/locator.js index b3bdb38..1537cee 100644 --- a/core/schema/filter/boolean/locator.js +++ b/core/schema/filter/boolean/locator.js @@ -1,11 +1,11 @@ -const SchemaFilterBoolean = require('.') - -class SchemaFilterBooleanLocator -{ - locate() - { - return new SchemaFilterBoolean - } -} - -module.exports = SchemaFilterBooleanLocator +const SchemaFilterBoolean = require('.') + +class SchemaFilterBooleanLocator +{ + locate() + { + return new SchemaFilterBoolean + } +} + +module.exports = SchemaFilterBooleanLocator diff --git a/core/schema/filter/csv/index.js b/core/schema/filter/csv/index.js index cd914ca..021d2ff 100644 --- a/core/schema/filter/csv/index.js +++ b/core/schema/filter/csv/index.js @@ -1,46 +1,46 @@ -/** - * @implements {SchemaFilter} - */ -class SchemaFilterCsv -{ - filter(options, data) - { - return options.collection - ? this.filterCollection(options, data) - : this.filterSingle(options, data) - } - - filterCollection(options, data) - { - if(!Array.isArray(data)) - return data - - const collection = [] - - for(const item of data) - { - const filtered = this.filterSingle(options, item) - collection.push(filtered) - } - - return collection - } - - filterSingle(options, data) - { - if(typeof data === 'string') - { - if(options.uppercase) - data = data.toUpperCase() - - if(options.lowercase) - data = data.toLowerCase() - - data = data.split(',') - } - - return data - } -} - -module.exports = SchemaFilterCsv +/** + * @implements {SchemaFilter} + */ +class SchemaFilterCsv +{ + filter(options, data) + { + return options.collection + ? this.filterCollection(options, data) + : this.filterSingle(options, data) + } + + filterCollection(options, data) + { + if(!Array.isArray(data)) + return data + + const collection = [] + + for(const item of data) + { + const filtered = this.filterSingle(options, item) + collection.push(filtered) + } + + return collection + } + + filterSingle(options, data) + { + if(typeof data === 'string') + { + if(options.uppercase) + data = data.toUpperCase() + + if(options.lowercase) + data = data.toLowerCase() + + data = data.split(',') + } + + return data + } +} + +module.exports = SchemaFilterCsv diff --git a/core/schema/filter/csv/locator.js b/core/schema/filter/csv/locator.js index e5d55ad..112120d 100644 --- a/core/schema/filter/csv/locator.js +++ b/core/schema/filter/csv/locator.js @@ -1,11 +1,11 @@ -const SchemaFilterCsv = require('.') - -class SchemaFilterCsvLocator -{ - locate() - { - return new SchemaFilterCsv - } -} - -module.exports = SchemaFilterCsvLocator +const SchemaFilterCsv = require('.') + +class SchemaFilterCsvLocator +{ + locate() + { + return new SchemaFilterCsv + } +} + +module.exports = SchemaFilterCsvLocator diff --git a/core/schema/filter/decimal/index.js b/core/schema/filter/decimal/index.js index 3528b7d..4486e96 100644 --- a/core/schema/filter/decimal/index.js +++ b/core/schema/filter/decimal/index.js @@ -1,38 +1,38 @@ -/** - * @implements {SchemaFilter} - */ -class SchemaFilterDecimal -{ - filter(options, data) - { - return options.collection - ? this.filterCollection(data) - : this.filterSingle(data) - } - - filterCollection(data) - { - if(!Array.isArray(data)) - return data - - const collection = [] - - for(const item of data) - { - const filtered = this.filterSingle(item) - collection.push(filtered) - } - - return collection - } - - filterSingle(data) - { - if(isNaN(data) === false) - return +data - - return data - } -} - -module.exports = SchemaFilterDecimal +/** + * @implements {SchemaFilter} + */ +class SchemaFilterDecimal +{ + filter(options, data) + { + return options.collection + ? this.filterCollection(data) + : this.filterSingle(data) + } + + filterCollection(data) + { + if(!Array.isArray(data)) + return data + + const collection = [] + + for(const item of data) + { + const filtered = this.filterSingle(item) + collection.push(filtered) + } + + return collection + } + + filterSingle(data) + { + if(isNaN(data) === false) + return +data + + return data + } +} + +module.exports = SchemaFilterDecimal diff --git a/core/schema/filter/decimal/locator.js b/core/schema/filter/decimal/locator.js index f6311e2..92408b2 100644 --- a/core/schema/filter/decimal/locator.js +++ b/core/schema/filter/decimal/locator.js @@ -1,11 +1,11 @@ -const SchemaFilterDecimal = require('.') - -class SchemaFilterDecimalLocator -{ - locate() - { - return new SchemaFilterDecimal - } -} - -module.exports = SchemaFilterDecimalLocator +const SchemaFilterDecimal = require('.') + +class SchemaFilterDecimalLocator +{ + locate() + { + return new SchemaFilterDecimal + } +} + +module.exports = SchemaFilterDecimalLocator diff --git a/core/schema/filter/index.js b/core/schema/filter/index.js index 8ad8bff..1aa2088 100644 --- a/core/schema/filter/index.js +++ b/core/schema/filter/index.js @@ -1,10 +1,10 @@ -/** - * @interface SchemaValidator - */ - -/** - * @function SchemaFilter#filter - * @param {Object} options - * @param {*} data - * @returns {void} - */ +/** + * @interface SchemaValidator + */ + +/** + * @function SchemaFilter#filter + * @param {Object} options + * @param {*} data + * @returns {void} + */ diff --git a/core/schema/filter/integer/index.js b/core/schema/filter/integer/index.js index c811843..e280638 100644 --- a/core/schema/filter/integer/index.js +++ b/core/schema/filter/integer/index.js @@ -1,38 +1,38 @@ -/** - * @implements {SchemaFilter} - */ -class SchemaFilterInteger -{ - filter(options, data) - { - return options.collection - ? this.filterCollection(data) - : this.filterSingle(data) - } - - filterCollection(data) - { - if(!Array.isArray(data)) - return data - - const collection = [] - - for(const item of data) - { - const filtered = this.filterSingle(item) - collection.push(filtered) - } - - return collection - } - - filterSingle(data) - { - if(isNaN(data) === false) - return +data - - return data - } -} - -module.exports = SchemaFilterInteger +/** + * @implements {SchemaFilter} + */ +class SchemaFilterInteger +{ + filter(options, data) + { + return options.collection + ? this.filterCollection(data) + : this.filterSingle(data) + } + + filterCollection(data) + { + if(!Array.isArray(data)) + return data + + const collection = [] + + for(const item of data) + { + const filtered = this.filterSingle(item) + collection.push(filtered) + } + + return collection + } + + filterSingle(data) + { + if(isNaN(data) === false) + return +data + + return data + } +} + +module.exports = SchemaFilterInteger diff --git a/core/schema/filter/integer/locator.js b/core/schema/filter/integer/locator.js index 3cd7836..a78105c 100644 --- a/core/schema/filter/integer/locator.js +++ b/core/schema/filter/integer/locator.js @@ -1,11 +1,11 @@ -const SchemaFilterInteger = require('.') - -class SchemaFilterIntegerLocator -{ - locate() - { - return new SchemaFilterInteger - } -} - -module.exports = SchemaFilterIntegerLocator +const SchemaFilterInteger = require('.') + +class SchemaFilterIntegerLocator +{ + locate() + { + return new SchemaFilterInteger + } +} + +module.exports = SchemaFilterIntegerLocator diff --git a/core/schema/filter/json/index.js b/core/schema/filter/json/index.js index 5e4ed93..79762a2 100644 --- a/core/schema/filter/json/index.js +++ b/core/schema/filter/json/index.js @@ -1,31 +1,31 @@ -/** - * @implements {SchemaFilter} - */ -class SchemaFilterJson -{ - filter(options, data) - { - try - { - if(options.stringified && typeof data === 'object') - { - return JSON.stringify(data, null, options.indentation) - } - - if(!options.stringified && typeof data === 'string') - { - return data ? JSON.parse(data) : {} - } - - return data - } - catch(error) - { - // it's not up to the filter to validate - // if we can't filter the data, then we simply pass the data forward - return data - } - } -} - -module.exports = SchemaFilterJson +/** + * @implements {SchemaFilter} + */ +class SchemaFilterJson +{ + filter(options, data) + { + try + { + if(options.stringified && typeof data === 'object') + { + return JSON.stringify(data, null, options.indentation) + } + + if(!options.stringified && typeof data === 'string') + { + return data ? JSON.parse(data) : {} + } + + return data + } + catch(error) + { + // it's not up to the filter to validate + // if we can't filter the data, then we simply pass the data forward + return data + } + } +} + +module.exports = SchemaFilterJson diff --git a/core/schema/filter/json/locator.js b/core/schema/filter/json/locator.js index 63dbc9e..38b36ce 100644 --- a/core/schema/filter/json/locator.js +++ b/core/schema/filter/json/locator.js @@ -1,11 +1,11 @@ -const SchemaFilterJson = require('.') - -class SchemaFilterJsonLocator -{ - locate() - { - return new SchemaFilterJson - } -} - -module.exports = SchemaFilterJsonLocator +const SchemaFilterJson = require('.') + +class SchemaFilterJsonLocator +{ + locate() + { + return new SchemaFilterJson + } +} + +module.exports = SchemaFilterJsonLocator diff --git a/core/schema/filter/phone-number/index.js b/core/schema/filter/phone-number/index.js new file mode 100644 index 0000000..84e26f2 --- /dev/null +++ b/core/schema/filter/phone-number/index.js @@ -0,0 +1,36 @@ +/** + * @implements {SchemaFilter} + */ +class SchemaFilterPhoneNumber +{ + filter(options, data) + { + return options.collection + ? this.filterCollection(data) + : this.filterSingle(data) + } + + filterCollection(data) + { + if(!Array.isArray(data)) + return data + + const collection = [] + + for(const item of data) + { + const filtered = this.filterSingle(item) + collection.push(filtered) + } + + return collection + } + + filterSingle(data) + { + data = `${data}`.replace(/[^\+0-9]/g, '') + return data + } +} + +module.exports = SchemaFilterPhoneNumber diff --git a/core/schema/filter/phone-number/locator.js b/core/schema/filter/phone-number/locator.js new file mode 100644 index 0000000..25364fc --- /dev/null +++ b/core/schema/filter/phone-number/locator.js @@ -0,0 +1,11 @@ +const SchemaFilterPhoneNumber = require('.') + +class SchemaFilterPhoneNumberLocator +{ + locate() + { + return new SchemaFilterPhoneNumber + } +} + +module.exports = SchemaFilterPhoneNumber diff --git a/core/schema/filter/schema/error/missing-schema-definition.js b/core/schema/filter/schema/error/missing-schema-definition.js index 7b93b01..7939a40 100644 --- a/core/schema/filter/schema/error/missing-schema-definition.js +++ b/core/schema/filter/schema/error/missing-schema-definition.js @@ -1,13 +1,13 @@ -/** - * @extends {Error} - */ -class MissingSchemaDefinitionError extends Error -{ - constructor(...a) - { - super(...a) - this.code = 'E_MISSING_SCHEMA_DEFINITION' - } -} - -module.exports = MissingSchemaDefinitionError +/** + * @extends {Error} + */ +class MissingSchemaDefinitionError extends Error +{ + constructor(...a) + { + super(...a) + this.code = 'E_MISSING_SCHEMA_DEFINITION' + } +} + +module.exports = MissingSchemaDefinitionError diff --git a/core/schema/filter/schema/index.js b/core/schema/filter/schema/index.js index a20e4d8..f58e464 100644 --- a/core/schema/filter/schema/index.js +++ b/core/schema/filter/schema/index.js @@ -1,51 +1,51 @@ -const MissingSchemaDefinitionError = require('./error/missing-schema-definition') -/** - * @implements {SchemaFilter} - */ -class SchemaFilterSchema -{ - constructor(composer) - { - this.composer = composer - } - - filter(options, data) - { - return options.collection - ? this.filterCollection(options, data) - : this.filterSingle(options, data) - } - - filterCollection(options, data) - { - if(!Array.isArray(data)) - return data - - const collection = [] - - for(const item of data) - { - const filtered = this.filterSingle(options, item) - collection.push(filtered) - } - - return collection - } - - filterSingle(options, data) - { - if(typeof options.schema === 'string') - { - return options.trait - ? this.composer.trait(options.schema, options.trait, data) - : this.composer.compose(options.schema, data) - } - else - { - const msg = `Expected the attribute "schema" to declare what type of schema ` - throw new MissingSchemaDefinitionError(msg) - } - } -} - -module.exports = SchemaFilterSchema +const MissingSchemaDefinitionError = require('./error/missing-schema-definition') +/** + * @implements {SchemaFilter} + */ +class SchemaFilterSchema +{ + constructor(composer) + { + this.composer = composer + } + + filter(options, data) + { + return options.collection + ? this.filterCollection(options, data) + : this.filterSingle(options, data) + } + + filterCollection(options, data) + { + if(!Array.isArray(data)) + return data + + const collection = [] + + for(const item of data) + { + const filtered = this.filterSingle(options, item) + collection.push(filtered) + } + + return collection + } + + filterSingle(options, data) + { + if(typeof options.schema === 'string') + { + return options.trait + ? this.composer.trait(options.schema, options.trait, data) + : this.composer.compose(options.schema, data) + } + else + { + const msg = `Expected the attribute "schema" to declare what type of schema ` + throw new MissingSchemaDefinitionError(msg) + } + } +} + +module.exports = SchemaFilterSchema diff --git a/core/schema/filter/schema/locator.js b/core/schema/filter/schema/locator.js index 05f5817..d4e5bf1 100644 --- a/core/schema/filter/schema/locator.js +++ b/core/schema/filter/schema/locator.js @@ -1,17 +1,17 @@ -const SchemaFilterSchema = require('.') - -class SchemaFilterSchemaLocator -{ - constructor(locator) - { - this.locator = locator - } - - locate() - { - const composer = this.locator.locate('core/schema/composer') - return new SchemaFilterSchema(composer) - } -} - -module.exports = SchemaFilterSchemaLocator +const SchemaFilterSchema = require('.') + +class SchemaFilterSchemaLocator +{ + constructor(locator) + { + this.locator = locator + } + + locate() + { + const composer = this.locator.locate('core/schema/composer') + return new SchemaFilterSchema(composer) + } +} + +module.exports = SchemaFilterSchemaLocator diff --git a/core/schema/filter/string/index.js b/core/schema/filter/string/index.js index 61f03b4..b64456a 100644 --- a/core/schema/filter/string/index.js +++ b/core/schema/filter/string/index.js @@ -1,50 +1,50 @@ -/** - * @implements {SchemaFilter} - */ -class SchemaFilterString -{ - filter(options, data) - { - return options.collection - ? this.filterCollection(options, data) - : this.filterSingle(options, data) - } - - filterCollection(options, data) - { - if(!Array.isArray(data)) - return data - - const collection = [] - - for(const item of data) - { - const filtered = this.filterSingle(options, item) - collection.push(filtered) - } - - return collection - } - - filterSingle(options, data) - { - if(typeof data === 'number') - data = `${data}` - - if(typeof data === 'boolean') - data = `${data}` - - if(typeof data === 'string') - { - if(options.uppercase) - data = data.toUpperCase() - - if(options.lowercase) - data = data.toLowerCase() - } - - return data - } -} - -module.exports = SchemaFilterString +/** + * @implements {SchemaFilter} + */ +class SchemaFilterString +{ + filter(options, data) + { + return options.collection + ? this.filterCollection(options, data) + : this.filterSingle(options, data) + } + + filterCollection(options, data) + { + if(!Array.isArray(data)) + return data + + const collection = [] + + for(const item of data) + { + const filtered = this.filterSingle(options, item) + collection.push(filtered) + } + + return collection + } + + filterSingle(options, data) + { + if(typeof data === 'number') + data = `${data}` + + if(typeof data === 'boolean') + data = `${data}` + + if(typeof data === 'string') + { + if(options.uppercase) + data = data.toUpperCase() + + if(options.lowercase) + data = data.toLowerCase() + } + + return data + } +} + +module.exports = SchemaFilterString diff --git a/core/schema/filter/string/locator.js b/core/schema/filter/string/locator.js index 23b7122..afd5f04 100644 --- a/core/schema/filter/string/locator.js +++ b/core/schema/filter/string/locator.js @@ -1,11 +1,11 @@ -const SchemaFilterString = require('.') - -class SchemaFilterStringLocator -{ - locate() - { - return new SchemaFilterString - } -} - -module.exports = SchemaFilterStringLocator +const SchemaFilterString = require('.') + +class SchemaFilterStringLocator +{ + locate() + { + return new SchemaFilterString + } +} + +module.exports = SchemaFilterStringLocator diff --git a/core/schema/filter/timestamp/index.js b/core/schema/filter/timestamp/index.js index f3423e7..0009906 100644 --- a/core/schema/filter/timestamp/index.js +++ b/core/schema/filter/timestamp/index.js @@ -1,46 +1,46 @@ -/** - * @implements {SchemaFilter} - */ -class SchemaFilterTimestamp -{ - filter(options, data) - { - return options.collection - ? this.filterCollection(options, data) - : this.filterSingle(options, data) - } - - filterCollection(options, data) - { - if(!Array.isArray(data)) - return data - - const collection = [] - - for(const item of data) - { - const filtered = this.filterSingle(options, item) - collection.push(filtered) - } - - return collection - } - - filterSingle(options, data) - { - const intData = parseInt(data) - - if(intData == data) - data = intData - - if(options.utc) - data = new Date(data).toUTCString() - - if(options.json) - data = new Date(data).toJSON() - - return data - } -} - -module.exports = SchemaFilterTimestamp +/** + * @implements {SchemaFilter} + */ +class SchemaFilterTimestamp +{ + filter(options, data) + { + return options.collection + ? this.filterCollection(options, data) + : this.filterSingle(options, data) + } + + filterCollection(options, data) + { + if(!Array.isArray(data)) + return data + + const collection = [] + + for(const item of data) + { + const filtered = this.filterSingle(options, item) + collection.push(filtered) + } + + return collection + } + + filterSingle(options, data) + { + const intData = parseInt(data) + + if(intData == data) + data = intData + + if(options.utc) + data = new Date(data).toUTCString() + + if(options.json) + data = new Date(data).toJSON() + + return data + } +} + +module.exports = SchemaFilterTimestamp diff --git a/core/schema/filter/timestamp/locator.js b/core/schema/filter/timestamp/locator.js index b46a89e..4fab138 100644 --- a/core/schema/filter/timestamp/locator.js +++ b/core/schema/filter/timestamp/locator.js @@ -1,11 +1,11 @@ -const SchemaFilterTimestamp = require('.') - -class SchemaFilterTimestampLocator -{ - locate() - { - return new SchemaFilterTimestamp - } -} - -module.exports = SchemaFilterTimestampLocator +const SchemaFilterTimestamp = require('.') + +class SchemaFilterTimestampLocator +{ + locate() + { + return new SchemaFilterTimestamp + } +} + +module.exports = SchemaFilterTimestampLocator diff --git a/core/schema/validator/boolean/error/invalid.js b/core/schema/validator/boolean/error/invalid.js index 206bd3e..e177a5b 100644 --- a/core/schema/validator/boolean/error/invalid.js +++ b/core/schema/validator/boolean/error/invalid.js @@ -1,13 +1,13 @@ -/** - * @extends {Error} - */ -class InvalidBooleanError extends Error -{ - constructor(...a) - { - super(...a) - this.code = 'E_INVALID_BOOLEAN' - } -} - -module.exports = InvalidBooleanError +/** + * @extends {Error} + */ +class InvalidBooleanError extends Error +{ + constructor(...a) + { + super(...a) + this.code = 'E_INVALID_BOOLEAN' + } +} + +module.exports = InvalidBooleanError diff --git a/core/schema/validator/boolean/index.js b/core/schema/validator/boolean/index.js index 47dd5af..de44ea4 100644 --- a/core/schema/validator/boolean/index.js +++ b/core/schema/validator/boolean/index.js @@ -1,17 +1,17 @@ -const InvalidBooleanError = require('./error/invalid') -/** - * @implements {SchemaValidator} - */ -class SchemaValidatorBoolean -{ - valid(options, data) - { - if(typeof data !== 'boolean') - { - const msg = `Invalid type: "${typeof data}", boolean expected` - throw new InvalidBooleanError(msg) - } - } -} - -module.exports = SchemaValidatorBoolean +const InvalidBooleanError = require('./error/invalid') +/** + * @implements {SchemaValidator} + */ +class SchemaValidatorBoolean +{ + valid(options, data) + { + if(typeof data !== 'boolean') + { + const msg = `Invalid type: "${typeof data}", boolean expected` + throw new InvalidBooleanError(msg) + } + } +} + +module.exports = SchemaValidatorBoolean diff --git a/core/schema/validator/boolean/locator.js b/core/schema/validator/boolean/locator.js index aeee1ba..124fc09 100644 --- a/core/schema/validator/boolean/locator.js +++ b/core/schema/validator/boolean/locator.js @@ -1,11 +1,11 @@ -const SchemaValidatorBoolean = require('.') - -class SchemaValidatorBooleanLocator -{ - locate() - { - return new SchemaValidatorBoolean - } -} - -module.exports = SchemaValidatorBooleanLocator +const SchemaValidatorBoolean = require('.') + +class SchemaValidatorBooleanLocator +{ + locate() + { + return new SchemaValidatorBoolean + } +} + +module.exports = SchemaValidatorBooleanLocator diff --git a/core/schema/validator/csv/error/invalid.js b/core/schema/validator/csv/error/invalid.js index 951c9c2..85e34c8 100644 --- a/core/schema/validator/csv/error/invalid.js +++ b/core/schema/validator/csv/error/invalid.js @@ -1,13 +1,13 @@ -/** - * @extends {Error} - */ -class InvalidCsvError extends Error -{ - constructor(...a) - { - super(...a) - this.code = 'E_INVALID_CSV' - } -} - -module.exports = InvalidCsvError +/** + * @extends {Error} + */ +class InvalidCsvError extends Error +{ + constructor(...a) + { + super(...a) + this.code = 'E_INVALID_CSV' + } +} + +module.exports = InvalidCsvError diff --git a/core/schema/validator/csv/index.js b/core/schema/validator/csv/index.js index 159fc66..e98c894 100644 --- a/core/schema/validator/csv/index.js +++ b/core/schema/validator/csv/index.js @@ -1,59 +1,59 @@ -const InvalidCsvError = require('./error/invalid') -/** - * @implements {SchemaValidator} - */ -class SchemaValidatorCsv -{ - valid(options, data) - { - if(Array.isArray(data) === false) - { - const msg = `Invalid type: "${typeof data}", csv (comma seperated values) string expected` - throw new InvalidCsvError(msg) - } - - if(options['not-empty'] - &&!data.length) - { - const msg = `Must not be empty` - throw new InvalidCsvError(msg) - } - - if('min' in options - && data.length < options.min) - { - const msg = `Length of values must be minimum: "${options.min}" long` - throw new InvalidCsvError(msg) - } - - if('max' in options - && data.length > options.max) - { - const msg = `Length of values can't be more then: "${options.max}" long` - throw new InvalidCsvError(msg) - } - - if(options.enum - &&!data.every((value) => options.enum.includes(value))) - { - const msg = `Expected all values of the csv to be one of the enumeral values: "${options.enum}"` - throw new InvalidCsvError(msg) - } - - if(options.uppercase - &&!data.every((value) => value === data.toUpperCase())) - { - const msg = `Upper case string expected` - throw new InvalidCsvError(msg) - } - - if(options.lowercase - &&!data.every((value) => value === data.toLowerCase())) - { - const msg = `Lower case string expected` - throw new InvalidCsvError(msg) - } - } -} - -module.exports = SchemaValidatorCsv +const InvalidCsvError = require('./error/invalid') +/** + * @implements {SchemaValidator} + */ +class SchemaValidatorCsv +{ + valid(options, data) + { + if(Array.isArray(data) === false) + { + const msg = `Invalid type: "${typeof data}", csv (comma seperated values) string expected` + throw new InvalidCsvError(msg) + } + + if(options['not-empty'] + &&!data.length) + { + const msg = `Must not be empty` + throw new InvalidCsvError(msg) + } + + if('min' in options + && data.length < options.min) + { + const msg = `Length of values must be minimum: "${options.min}" long` + throw new InvalidCsvError(msg) + } + + if('max' in options + && data.length > options.max) + { + const msg = `Length of values can't be more then: "${options.max}" long` + throw new InvalidCsvError(msg) + } + + if(options.enum + &&!data.every((value) => options.enum.includes(value))) + { + const msg = `Expected all values of the csv to be one of the enumeral values: "${options.enum}"` + throw new InvalidCsvError(msg) + } + + if(options.uppercase + &&!data.every((value) => value === data.toUpperCase())) + { + const msg = `Upper case string expected` + throw new InvalidCsvError(msg) + } + + if(options.lowercase + &&!data.every((value) => value === data.toLowerCase())) + { + const msg = `Lower case string expected` + throw new InvalidCsvError(msg) + } + } +} + +module.exports = SchemaValidatorCsv diff --git a/core/schema/validator/csv/locator.js b/core/schema/validator/csv/locator.js index affff60..038ce53 100644 --- a/core/schema/validator/csv/locator.js +++ b/core/schema/validator/csv/locator.js @@ -1,11 +1,11 @@ -const SchemaValidatorString = require('.') - -class SchemaValidatorStringLocator -{ - locate() - { - return new SchemaValidatorString - } -} - -module.exports = SchemaValidatorStringLocator +const SchemaValidatorString = require('.') + +class SchemaValidatorStringLocator +{ + locate() + { + return new SchemaValidatorString + } +} + +module.exports = SchemaValidatorStringLocator diff --git a/core/schema/validator/decimal/error/invalid.js b/core/schema/validator/decimal/error/invalid.js index 183c79f..ab23ecb 100644 --- a/core/schema/validator/decimal/error/invalid.js +++ b/core/schema/validator/decimal/error/invalid.js @@ -1,13 +1,13 @@ -/** - * @extends {Error} - */ -class InvalidDecimalError extends Error -{ - constructor(...a) - { - super(...a) - this.code = 'E_INVALID_DECIMAL' - } -} - -module.exports = InvalidDecimalError +/** + * @extends {Error} + */ +class InvalidDecimalError extends Error +{ + constructor(...a) + { + super(...a) + this.code = 'E_INVALID_DECIMAL' + } +} + +module.exports = InvalidDecimalError diff --git a/core/schema/validator/decimal/index.js b/core/schema/validator/decimal/index.js index e36362c..6fc8a26 100644 --- a/core/schema/validator/decimal/index.js +++ b/core/schema/validator/decimal/index.js @@ -1,59 +1,59 @@ -const InvalidDecimalError = require('./error/invalid') -/** - * @implements {SchemaValidator} - */ -class SchemaValidatorDecimal -{ - valid(options, data) - { - if(typeof data !== 'number') - { - const msg = `Invalid type: "${typeof data}", number expected` - throw new InvalidDecimalError(msg) - } - - if(options.unsigned - && data < 0) - { - const msg = `Expected an unsigned decimal` - throw new InvalidDecimalError(msg) - } - - if('min' in options - && data < options.min) - { - const msg = `Decimal must be minimum: "${options.min}"` - throw new InvalidDecimalError(msg) - } - - if('max' in options - && data > options.max) - { - const msg = `Decimal can't be more then: "${options.max}"` - throw new InvalidDecimalError(msg) - } - - if('gt' in options - && data > options.gt) - { - const msg = `Decimal must be more then: "${options.gt}"` - throw new InvalidDecimalError(msg) - } - - if('lt' in options - && data < options.lt) - { - const msg = `Decimal must be less then: "${options.lt}"` - throw new InvalidDecimalError(msg) - } - - if(options.enum - &&!options.enum.includes(data)) - { - const msg = `Expected one of the enumeral values: "${options.enum}"` - throw new InvalidDecimalError(msg) - } - } -} - -module.exports = SchemaValidatorDecimal +const InvalidDecimalError = require('./error/invalid') +/** + * @implements {SchemaValidator} + */ +class SchemaValidatorDecimal +{ + valid(options, data) + { + if(typeof data !== 'number') + { + const msg = `Invalid type: "${typeof data}", number expected` + throw new InvalidDecimalError(msg) + } + + if(options.unsigned + && data < 0) + { + const msg = `Expected an unsigned decimal` + throw new InvalidDecimalError(msg) + } + + if('min' in options + && data < options.min) + { + const msg = `Decimal must be minimum: "${options.min}"` + throw new InvalidDecimalError(msg) + } + + if('max' in options + && data > options.max) + { + const msg = `Decimal can't be more then: "${options.max}"` + throw new InvalidDecimalError(msg) + } + + if('gt' in options + && data > options.gt) + { + const msg = `Decimal must be more then: "${options.gt}"` + throw new InvalidDecimalError(msg) + } + + if('lt' in options + && data < options.lt) + { + const msg = `Decimal must be less then: "${options.lt}"` + throw new InvalidDecimalError(msg) + } + + if(options.enum + &&!options.enum.includes(data)) + { + const msg = `Expected one of the enumeral values: "${options.enum}"` + throw new InvalidDecimalError(msg) + } + } +} + +module.exports = SchemaValidatorDecimal diff --git a/core/schema/validator/decimal/locator.js b/core/schema/validator/decimal/locator.js index ff89896..e299ace 100644 --- a/core/schema/validator/decimal/locator.js +++ b/core/schema/validator/decimal/locator.js @@ -1,11 +1,11 @@ -const SchemaValidatorDecimal = require('.') - -class SchemaValidatorDecimalLocator -{ - locate() - { - return new SchemaValidatorDecimal - } -} - -module.exports = SchemaValidatorDecimalLocator +const SchemaValidatorDecimal = require('.') + +class SchemaValidatorDecimalLocator +{ + locate() + { + return new SchemaValidatorDecimal + } +} + +module.exports = SchemaValidatorDecimalLocator diff --git a/core/schema/validator/index.js b/core/schema/validator/index.js index 588f09a..a8b8aab 100644 --- a/core/schema/validator/index.js +++ b/core/schema/validator/index.js @@ -1,10 +1,10 @@ -/** - * @interface SchemaValidator - */ - -/** - * @function SchemaValidator#valid - * @param {Object} options - * @param {*} data - * @returns {void} - */ +/** + * @interface SchemaValidator + */ + +/** + * @function SchemaValidator#valid + * @param {Object} options + * @param {*} data + * @returns {void} + */ diff --git a/core/schema/validator/integer/error/invalid.js b/core/schema/validator/integer/error/invalid.js index f443f50..1b00961 100644 --- a/core/schema/validator/integer/error/invalid.js +++ b/core/schema/validator/integer/error/invalid.js @@ -1,13 +1,13 @@ -/** - * @extends {Error} - */ -class InvalidIntegerError extends Error -{ - constructor(...a) - { - super(...a) - this.code = 'E_INVALID_INTEGER' - } -} - -module.exports = InvalidIntegerError +/** + * @extends {Error} + */ +class InvalidIntegerError extends Error +{ + constructor(...a) + { + super(...a) + this.code = 'E_INVALID_INTEGER' + } +} + +module.exports = InvalidIntegerError diff --git a/core/schema/validator/integer/index.js b/core/schema/validator/integer/index.js index 3dfe533..9e19315 100644 --- a/core/schema/validator/integer/index.js +++ b/core/schema/validator/integer/index.js @@ -1,65 +1,65 @@ -const InvalidIntegerError = require('./error/invalid') -/** - * @implements {SchemaValidator} - */ -class SchemaValidatorInteger -{ - valid(options, data) - { - if(typeof data !== 'number') - { - const msg = `Invalid type: "${typeof data}", number expected` - throw new InvalidIntegerError(msg) - } - - if(data !== parseInt(data)) - { - const msg = `Integer expected, found decimals` - throw new InvalidIntegerError(msg) - } - - if(options.unsigned - && data < 0) - { - const msg = `Expected an unsigned integer ` - throw new InvalidIntegerError(msg) - } - - if('min' in options - && data < options.min) - { - const msg = `Integer must be minimum: "${options.min}"` - throw new InvalidIntegerError(msg) - } - - if('max' in options - && data > options.max) - { - const msg = `Integer can't be more then: "${options.max}"` - throw new InvalidIntegerError(msg) - } - - if('gt' in options - && data < options.gt) - { - const msg = `Integer must be more then: "${options.gt}"` - throw new InvalidIntegerError(msg) - } - - if('lt' in options - && data > options.lt) - { - const msg = `Integer must be less then: "${options.lt}"` - throw new InvalidIntegerError(msg) - } - - if(options.enum - &&!options.enum.includes(data)) - { - const msg = `Expected one of the enumeral values: "${options.enum}"` - throw new InvalidIntegerError(msg) - } - } -} - -module.exports = SchemaValidatorInteger +const InvalidIntegerError = require('./error/invalid') +/** + * @implements {SchemaValidator} + */ +class SchemaValidatorInteger +{ + valid(options, data) + { + if(typeof data !== 'number') + { + const msg = `Invalid type: "${typeof data}", number expected` + throw new InvalidIntegerError(msg) + } + + if(data !== parseInt(data)) + { + const msg = `Integer expected, found decimals` + throw new InvalidIntegerError(msg) + } + + if(options.unsigned + && data < 0) + { + const msg = `Expected an unsigned integer ` + throw new InvalidIntegerError(msg) + } + + if('min' in options + && data < options.min) + { + const msg = `Integer must be minimum: "${options.min}"` + throw new InvalidIntegerError(msg) + } + + if('max' in options + && data > options.max) + { + const msg = `Integer can't be more then: "${options.max}"` + throw new InvalidIntegerError(msg) + } + + if('gt' in options + && data < options.gt) + { + const msg = `Integer must be more then: "${options.gt}"` + throw new InvalidIntegerError(msg) + } + + if('lt' in options + && data > options.lt) + { + const msg = `Integer must be less then: "${options.lt}"` + throw new InvalidIntegerError(msg) + } + + if(options.enum + &&!options.enum.includes(data)) + { + const msg = `Expected one of the enumeral values: "${options.enum}"` + throw new InvalidIntegerError(msg) + } + } +} + +module.exports = SchemaValidatorInteger diff --git a/core/schema/validator/integer/locator.js b/core/schema/validator/integer/locator.js index 5e52b1e..58fefa1 100644 --- a/core/schema/validator/integer/locator.js +++ b/core/schema/validator/integer/locator.js @@ -1,11 +1,11 @@ -const SchemaValidatorInteger = require('.') - -class SchemaValidatorIntegerLocator -{ - locate() - { - return new SchemaValidatorInteger - } -} - -module.exports = SchemaValidatorIntegerLocator +const SchemaValidatorInteger = require('.') + +class SchemaValidatorIntegerLocator +{ + locate() + { + return new SchemaValidatorInteger + } +} + +module.exports = SchemaValidatorIntegerLocator diff --git a/core/schema/validator/json/error/invalid.js b/core/schema/validator/json/error/invalid.js index e16be82..03bca3f 100644 --- a/core/schema/validator/json/error/invalid.js +++ b/core/schema/validator/json/error/invalid.js @@ -1,13 +1,13 @@ -/** - * @extends {Error} - */ -class InvalidJsonError extends Error -{ - constructor(...a) - { - super(...a) - this.code = 'E_INVALID_JSON' - } -} - -module.exports = InvalidJsonError +/** + * @extends {Error} + */ +class InvalidJsonError extends Error +{ + constructor(...a) + { + super(...a) + this.code = 'E_INVALID_JSON' + } +} + +module.exports = InvalidJsonError diff --git a/core/schema/validator/json/index.js b/core/schema/validator/json/index.js index 405965e..a3dd4ba 100644 --- a/core/schema/validator/json/index.js +++ b/core/schema/validator/json/index.js @@ -1,23 +1,23 @@ -const InvalidJsonError = require('./error/invalid') -/** - * @implements {SchemaValidator} - */ -class SchemaValidatorJson -{ - valid(options, data) - { - try - { - options.stringified - ? JSON.parse(data) - : JSON.stringify(data) - } - catch(error) - { - const msg = `Unparsable JSON provided` - throw new InvalidJsonError(msg) - } - } -} - -module.exports = SchemaValidatorJson +const InvalidJsonError = require('./error/invalid') +/** + * @implements {SchemaValidator} + */ +class SchemaValidatorJson +{ + valid(options, data) + { + try + { + options.stringified + ? JSON.parse(data) + : JSON.stringify(data) + } + catch(error) + { + const msg = `Unparsable JSON provided` + throw new InvalidJsonError(msg) + } + } +} + +module.exports = SchemaValidatorJson diff --git a/core/schema/validator/json/locator.js b/core/schema/validator/json/locator.js index 89d8a0f..307fc4a 100644 --- a/core/schema/validator/json/locator.js +++ b/core/schema/validator/json/locator.js @@ -1,11 +1,11 @@ -const SchemaValidatorJson = require('.') - -class SchemaValidatorJsonLocator -{ - locate() - { - return new SchemaValidatorJson - } -} - -module.exports = SchemaValidatorJsonLocator +const SchemaValidatorJson = require('.') + +class SchemaValidatorJsonLocator +{ + locate() + { + return new SchemaValidatorJson + } +} + +module.exports = SchemaValidatorJsonLocator diff --git a/core/schema/validator/phone-number/error/invalid.js b/core/schema/validator/phone-number/error/invalid.js new file mode 100644 index 0000000..fbf6957 --- /dev/null +++ b/core/schema/validator/phone-number/error/invalid.js @@ -0,0 +1,13 @@ +/** + * @extends {Error} + */ +class InvalidPhoneNumberError extends Error +{ + constructor(...a) + { + super(...a) + this.code = 'E_INVALID_PHONE_NUMBER' + } +} + +module.exports = InvalidPhoneNumberError diff --git a/core/schema/validator/phone-number/index.js b/core/schema/validator/phone-number/index.js new file mode 100644 index 0000000..46f79e9 --- /dev/null +++ b/core/schema/validator/phone-number/index.js @@ -0,0 +1,67 @@ +const InvalidPhoneNumberError = require('./error/invalid') +/** + * @implements {SchemaValidator} + */ +class SchemaValidatorPhoneNumber +{ + valid(options, data) + { + if(typeof data !== 'string') + { + const msg = `Invalid type: "${typeof data}", string expected` + throw new InvalidPhoneNumberError(msg) + } + + const plusAppears = data.split('').reduce((count, char) => char === '+' ? ++count : count, 0) + + if(plusAppears > 1) + { + const msg = `Unexpected character in phone number: "${data}"` + throw new InvalidPhoneNumberError(msg) + } + + if(plusAppears === 1 + && data[0] !== '+') + { + const msg = `Plus character only allowed once in the beginning of the phone number: "${data}"` + throw new InvalidPhoneNumberError(msg) + } + + if('min' in options + && data.length < options.min) + { + const msg = `Phone numbers length must be minimum: "${options.min}"` + throw new InvalidPhoneNumberError(msg) + } + + if('max' in options + && data.length > options.max) + { + const msg = `Phone numbers length can't be more than: "${options.max}"` + throw new InvalidPhoneNumberError(msg) + } + + if('gt' in options + && data.length < options.gt) + { + const msg = `Phone numbers length must be more than: "${options.gt}"` + throw new InvalidPhoneNumberError(msg) + } + + if('lt' in options + && data.length > options.lt) + { + const msg = `Phone numbers length must be less than: "${options.lt}"` + throw new InvalidPhoneNumberError(msg) + } + + if(options.enum + &&!options.enum.includes(data)) + { + const msg = `Expected one of the enumeral values: "${options.enum}"` + throw new InvalidPhoneNumberError(msg) + } + } +} + +module.exports = SchemaValidatorPhoneNumber diff --git a/core/schema/validator/phone-number/locator.js b/core/schema/validator/phone-number/locator.js new file mode 100644 index 0000000..f046c49 --- /dev/null +++ b/core/schema/validator/phone-number/locator.js @@ -0,0 +1,11 @@ +const SchemaValidatorPhoneNumber = require('.') + +class SchemaValidatorPhoneNumberLocator +{ + locate() + { + return new SchemaValidatorPhoneNumber + } +} + +module.exports = SchemaValidatorPhoneNumberLocator diff --git a/core/schema/validator/schema/error/invalid.js b/core/schema/validator/schema/error/invalid.js index c676a4b..fb9ea41 100644 --- a/core/schema/validator/schema/error/invalid.js +++ b/core/schema/validator/schema/error/invalid.js @@ -1,13 +1,13 @@ -/** - * @extends {Error} - */ -class InvalidSchemaError extends Error -{ - constructor(...a) - { - super(...a) - this.code = 'E_INVALID_SCHEMA' - } -} - -module.exports = InvalidSchemaError +/** + * @extends {Error} + */ +class InvalidSchemaError extends Error +{ + constructor(...a) + { + super(...a) + this.code = 'E_INVALID_SCHEMA' + } +} + +module.exports = InvalidSchemaError diff --git a/core/schema/validator/schema/index.js b/core/schema/validator/schema/index.js index 8a6e271..acc525a 100644 --- a/core/schema/validator/schema/index.js +++ b/core/schema/validator/schema/index.js @@ -1,13 +1,13 @@ -const InvalidSchemaError = require('./error/invalid') -/** - * @implements {SchemaValidator} - */ -class SchemaValidatorSchema -{ - valid(options, data) - { - // nothing to validate - } -} - -module.exports = SchemaValidatorSchema +const InvalidSchemaError = require('./error/invalid') +/** + * @implements {SchemaValidator} + */ +class SchemaValidatorSchema +{ + valid(options, data) + { + // nothing to validate + } +} + +module.exports = SchemaValidatorSchema diff --git a/core/schema/validator/schema/locator.js b/core/schema/validator/schema/locator.js index aea56d3..a866bb5 100644 --- a/core/schema/validator/schema/locator.js +++ b/core/schema/validator/schema/locator.js @@ -1,11 +1,11 @@ -const SchemaValidatorSchema = require('.') - -class SchemaValidatorSchemaLocator -{ - locate() - { - return new SchemaValidatorSchema - } -} - -module.exports = SchemaValidatorSchemaLocator +const SchemaValidatorSchema = require('.') + +class SchemaValidatorSchemaLocator +{ + locate() + { + return new SchemaValidatorSchema + } +} + +module.exports = SchemaValidatorSchemaLocator diff --git a/core/schema/validator/string/error/invalid.js b/core/schema/validator/string/error/invalid.js index 484594e..e7203ce 100644 --- a/core/schema/validator/string/error/invalid.js +++ b/core/schema/validator/string/error/invalid.js @@ -1,13 +1,13 @@ -/** - * @extends {Error} - */ -class InvalidStringError extends Error -{ - constructor(...a) - { - super(...a) - this.code = 'E_INVALID_STRING' - } -} - -module.exports = InvalidStringError +/** + * @extends {Error} + */ +class InvalidStringError extends Error +{ + constructor(...a) + { + super(...a) + this.code = 'E_INVALID_STRING' + } +} + +module.exports = InvalidStringError diff --git a/core/schema/validator/string/index.js b/core/schema/validator/string/index.js index d457ad4..b681952 100644 --- a/core/schema/validator/string/index.js +++ b/core/schema/validator/string/index.js @@ -1,66 +1,66 @@ -const InvalidStringError = require('./error/invalid') -/** - * @implements {SchemaValidator} - */ -class SchemaValidatorString -{ - valid(options, data) - { - if(typeof data !== 'string') - { - const msg = `Invalid type: "${typeof data}", string expected` - throw new InvalidStringError(msg) - } - - if(options['not-empty'] - && !data.length) - { - const msg = `Must not be empty` - throw new InvalidStringError(msg) - } - - if('min' in options - && data.length < options.min) - { - const msg = `String length must be minimum: "${options.min}" long` - throw new InvalidStringError(msg) - } - - if('max' in options - && data.length > options.max) - { - const msg = `String length can't be more then: "${options.max}" long` - throw new InvalidStringError(msg) - } - - if('length' in options - && data.length !== options.length) - { - const msg = `String length must be ${options.length}` - throw new InvalidStringError(msg) - } - - if(options.enum - &&!options.enum.includes(data)) - { - const msg = `Expected one of the enumeral values: "${options.enum}"` - throw new InvalidStringError(msg) - } - - if(options.uppercase - && data !== data.toUpperCase()) - { - const msg = `Upper case string expected` - throw new InvalidStringError(msg) - } - - if(options.lowercase - && data !== data.toLowerCase()) - { - const msg = `Lower case string expected` - throw new InvalidStringError(msg) - } - } -} - -module.exports = SchemaValidatorString +const InvalidStringError = require('./error/invalid') +/** + * @implements {SchemaValidator} + */ +class SchemaValidatorString +{ + valid(options, data) + { + if(typeof data !== 'string') + { + const msg = `Invalid type: "${typeof data}", string expected` + throw new InvalidStringError(msg) + } + + if(options['not-empty'] + && !data.length) + { + const msg = `Must not be empty` + throw new InvalidStringError(msg) + } + + if('min' in options + && data.length < options.min) + { + const msg = `String length must be minimum: "${options.min}" long` + throw new InvalidStringError(msg) + } + + if('max' in options + && data.length > options.max) + { + const msg = `String length can't be more then: "${options.max}" long` + throw new InvalidStringError(msg) + } + + if('length' in options + && data.length !== options.length) + { + const msg = `String length must be ${options.length}` + throw new InvalidStringError(msg) + } + + if(options.enum + &&!options.enum.includes(data)) + { + const msg = `Expected one of the enumeral values: "${options.enum}"` + throw new InvalidStringError(msg) + } + + if(options.uppercase + && data !== data.toUpperCase()) + { + const msg = `Upper case string expected` + throw new InvalidStringError(msg) + } + + if(options.lowercase + && data !== data.toLowerCase()) + { + const msg = `Lower case string expected` + throw new InvalidStringError(msg) + } + } +} + +module.exports = SchemaValidatorString diff --git a/core/schema/validator/string/locator.js b/core/schema/validator/string/locator.js index affff60..038ce53 100644 --- a/core/schema/validator/string/locator.js +++ b/core/schema/validator/string/locator.js @@ -1,11 +1,11 @@ -const SchemaValidatorString = require('.') - -class SchemaValidatorStringLocator -{ - locate() - { - return new SchemaValidatorString - } -} - -module.exports = SchemaValidatorStringLocator +const SchemaValidatorString = require('.') + +class SchemaValidatorStringLocator +{ + locate() + { + return new SchemaValidatorString + } +} + +module.exports = SchemaValidatorStringLocator diff --git a/core/schema/validator/timestamp/error/invalid.js b/core/schema/validator/timestamp/error/invalid.js index 854f8b5..b0b0878 100644 --- a/core/schema/validator/timestamp/error/invalid.js +++ b/core/schema/validator/timestamp/error/invalid.js @@ -1,13 +1,13 @@ -/** - * @extends {Error} - */ -class InvalidTimestampError extends Error -{ - constructor(...a) - { - super(...a) - this.code = 'E_INVALID_TIMESTAMP' - } -} - -module.exports = InvalidTimestampError +/** + * @extends {Error} + */ +class InvalidTimestampError extends Error +{ + constructor(...a) + { + super(...a) + this.code = 'E_INVALID_TIMESTAMP' + } +} + +module.exports = InvalidTimestampError diff --git a/core/schema/validator/timestamp/index.js b/core/schema/validator/timestamp/index.js index 80dbcb3..7cd4845 100644 --- a/core/schema/validator/timestamp/index.js +++ b/core/schema/validator/timestamp/index.js @@ -1,54 +1,54 @@ -const InvalidTimestampError = require('./error/invalid') -/** - * @implements {SchemaValidator} - */ -class SchemaValidatorTimestamp -{ - valid(options, data) - { - const date = new Date(data) - - if(isNaN(date.getTime())) - { - const msg = `Value for timestamp is invalid: "${data}"` - throw new InvalidTimestampError(msg) - } - - if('min' in options - && date.getTime() < new Date(options.min).getTime()) - { - const msg = `Timestamp must be at least: "${options.min}"` - throw new InvalidTimestampError(msg) - } - - if('max' in options - && date.getTime() > new Date(options.max).getTime()) - { - const msg = `Timestamp can't be more then: "${options.max}"` - throw new InvalidTimestampError(msg) - } - - if('gt' in options - && date.getTime() > new Date(options.gt).getTime()) - { - const msg = `Timestamp must be more then: "${options.gt}" long` - throw new InvalidTimestampError(msg) - } - - if('lt' in options - && date.getTime() < new Date(options.lt).getTime()) - { - const msg = `Timestamp must be less then: "${options.lt}" long` - throw new InvalidTimestampError(msg) - } - - if(options.enum - &&!options.enum.includes(data)) - { - const msg = `Expected one of the enumeral values: "${options.enum}"` - throw new InvalidTimestampError(msg) - } - } -} - -module.exports = SchemaValidatorTimestamp +const InvalidTimestampError = require('./error/invalid') +/** + * @implements {SchemaValidator} + */ +class SchemaValidatorTimestamp +{ + valid(options, data) + { + const date = new Date(data) + + if(isNaN(date.getTime())) + { + const msg = `Value for timestamp is invalid: "${data}"` + throw new InvalidTimestampError(msg) + } + + if('min' in options + && date.getTime() < new Date(options.min).getTime()) + { + const msg = `Timestamp must be at least: "${options.min}"` + throw new InvalidTimestampError(msg) + } + + if('max' in options + && date.getTime() > new Date(options.max).getTime()) + { + const msg = `Timestamp can't be more then: "${options.max}"` + throw new InvalidTimestampError(msg) + } + + if('gt' in options + && date.getTime() > new Date(options.gt).getTime()) + { + const msg = `Timestamp must be more then: "${options.gt}" long` + throw new InvalidTimestampError(msg) + } + + if('lt' in options + && date.getTime() < new Date(options.lt).getTime()) + { + const msg = `Timestamp must be less then: "${options.lt}" long` + throw new InvalidTimestampError(msg) + } + + if(options.enum + &&!options.enum.includes(data)) + { + const msg = `Expected one of the enumeral values: "${options.enum}"` + throw new InvalidTimestampError(msg) + } + } +} + +module.exports = SchemaValidatorTimestamp diff --git a/core/schema/validator/timestamp/locator.js b/core/schema/validator/timestamp/locator.js index affff60..038ce53 100644 --- a/core/schema/validator/timestamp/locator.js +++ b/core/schema/validator/timestamp/locator.js @@ -1,11 +1,11 @@ -const SchemaValidatorString = require('.') - -class SchemaValidatorStringLocator -{ - locate() - { - return new SchemaValidatorString - } -} - -module.exports = SchemaValidatorStringLocator +const SchemaValidatorString = require('.') + +class SchemaValidatorStringLocator +{ + locate() + { + return new SchemaValidatorString + } +} + +module.exports = SchemaValidatorStringLocator diff --git a/core/string/config.js b/core/string/config.js index 142bc4a..ea45369 100644 --- a/core/string/config.js +++ b/core/string/config.js @@ -1,10 +1,10 @@ -module.exports = -{ - core: - { - locator: - { - 'core/string' : __dirname - } - } -} +module.exports = +{ + core: + { + locator: + { + 'core/string' : __dirname + } + } +} diff --git a/core/string/index.js b/core/string/index.js index babb828..9d90aef 100644 --- a/core/string/index.js +++ b/core/string/index.js @@ -1,39 +1,39 @@ -class CoreString -{ - /** - * @example "foobar" => "Foobar" - * @param {string} s input to be manipulated - * @returns {string} - */ - composeFirstUpperCase(s) - { - return s[0].toUpperCase() + s.slice(1) - } - - /** - * @example "Foo BAR baz" => "foo-bar-baz" - * @param {string} s input to be manipulated - * @param {string} [seperator='-'] string to be used as the seperator - * @returns {string} A string that has replaced the spaces with dashes and lowercased the string - */ - composeSeperatedLowerCase(s, seperator = '-') - { - return s.replace(/\W+/g, seperator).toLowerCase() - } - - /** - * @example "Foo-BAR-baz" => "fooBarBaz" or "Foo BAR baz" => "fooBarBaz" - * @param {string} s input to be manipulated - * @param {string} [seperator='-'] string to be used as the seperator - * @returns {string} A string that has replaced the spaces with dashes and lowercased the string - */ - composeCamelCase(s, seperator = '-') - { - s = this.composeSeperatedLowerCase(s) - s = s.split(seperator).map((part, i) => i === 0 ? part : this.composeFirstUpperCase(part)).join('') - - return s - } -} - -module.exports = CoreString +class CoreString +{ + /** + * @example "foobar" => "Foobar" + * @param {string} s input to be manipulated + * @returns {string} + */ + composeFirstUpperCase(s) + { + return s[0].toUpperCase() + s.slice(1) + } + + /** + * @example "Foo BAR baz" => "foo-bar-baz" + * @param {string} s input to be manipulated + * @param {string} [seperator='-'] string to be used as the seperator + * @returns {string} A string that has replaced the spaces with dashes and lowercased the string + */ + composeSeperatedLowerCase(s, seperator = '-') + { + return s.replace(/\W+/g, seperator).toLowerCase() + } + + /** + * @example "Foo-BAR-baz" => "fooBarBaz" or "Foo BAR baz" => "fooBarBaz" + * @param {string} s input to be manipulated + * @param {string} [seperator='-'] string to be used as the seperator + * @returns {string} A string that has replaced the spaces with dashes and lowercased the string + */ + composeCamelCase(s, seperator = '-') + { + s = this.composeSeperatedLowerCase(s) + s = s.split(seperator).map((part, i) => i === 0 ? part : this.composeFirstUpperCase(part)).join('') + + return s + } +} + +module.exports = CoreString diff --git a/core/string/locator.js b/core/string/locator.js index 45f3918..063a8ba 100644 --- a/core/string/locator.js +++ b/core/string/locator.js @@ -1,11 +1,11 @@ -const CoreString = require('.') - -class CoreStringLocator -{ - locate() - { - return new CoreString - } -} - -module.exports = CoreStringLocator +const CoreString = require('.') + +class CoreStringLocator +{ + locate() + { + return new CoreString + } +} + +module.exports = CoreStringLocator diff --git a/doc/README-1.8.md b/doc/README-1.8.md index efb3560..30060bb 100644 --- a/doc/README-1.8.md +++ b/doc/README-1.8.md @@ -1,811 +1,811 @@ -# Core - -Licence: [MIT](https://opensource.org/licenses/MIT) - ---- - -- A core framework I use when developing in [nodejs](https://nodejs.org/en/docs/). -- I built the framework to help me build applications after a reactive [domain driven design (DDD)](https://en.wikipedia.org/wiki/Domain-driven_design) approch. -- The framework is designed to have little to no production dependencies. -- The framework offers solutions for different topics, not necessarily an http server. -- The vision of the framework is to offer a code structure to the developer that will help segregate responsibilities in projects through a [SOLID](https://en.wikipedia.org/wiki/SOLID) [OOP](https://en.wikipedia.org/wiki/Object-oriented_programming) approach. - -## Addons - -Consider extending the framework with one or multiple plugins where the extra functionality is required: -*Please note; you should use matching versions for core and the plugins by major and future releases* - -- [core-handlebars](http://github.com/superhero/js.core.handlebars) -- [core-resource](http://github.com/superhero/js.core.resource) -- [core-websocket](http://github.com/superhero/js.core.websocket) - -## Install - -`npm install @superhero/core` - -...or just set the dependency in your `package.json` file: - -```json -{ - "dependencies": - { - "@superhero/core": "*" - } -} -``` - -## Example Application - -An example application to get started, covering some recommended "best practices" regarding testing and docs generation. - -The application is a calculator with very basic functionality. The aim with this application is not to make a good calculator. The aim is to show you, who is reading this, how different parts of the framework works, or are intended to work. - -### Example Application › File structure - -``` -super-duper-app -├── docs -├── src -│ ├── api -│ │ ├── endpoint -│ │ │ ├── append-calculation.js -│ │ │ └── create-calculation.js -│ │ ├── middleware -│ │ │ └── authentication.js -│ │ └── config.js -│ ├── calculator -│ │ ├── error -│ │ | ├── calculation-could-not-be-found.js -│ │ | └── invalid-calculation-type.js -│ │ ├── calculation.js -│ │ ├── config.js -│ │ ├── index.js -│ │ └── locator.js -│ ├── logger -│ │ ├── config.js -│ │ ├── index.js -│ │ └── locator.js -│ └── index.js -├── test -│ ├── init.js -│ ├── mocha.opts -│ ├── test.calculations.js -│ └── test.logger.js -├── .gitignore -├── package.json -└── README.md -``` - -The file structure is here divided to only 3 modules - -- One called `api` *- that's responsible for the endpoints.* -- A second one called `calculator` *- that is responsible for domain logic.* -- The third one called `logger` *- an infrastructure layer that will log events to the console.* - -#### `.gitignore` - -``` -docs/generated -npm-debug.log -node_modules -package-lock.json -.nyc_output -``` - -Set up a `.gitignore` file to ignore some auto-generated files to keep a clean repository. - -#### `package.json` - -```js -{ - "name": "Super Duper App", - "version": "0.0.1", - "description": "An example application of the superhero/core framework", - "repository": "https://github.com/...", - "license": "MIT", - "main": "src/index.js", - "author": { - "name": "Padawan", - "email": "padawan@example.com" - }, - "scripts": { - "docs-coverage": "nyc mocha './test/test.*.js' --opts ./test/mocha.opts && nyc report --reporter=html --report-dir=./docs/generated/coverage", - "docs-tests": "mocha './test/test.*.js' --opts ./test/mocha.opts --reporter mochawesome --reporter-options reportDir=docs/generated/test,reportFilename=index,showHooks=always", - "test": "nyc mocha './test/test.*.js' --opts ./test/mocha.opts", - "start": "node ./src/index.js" - }, - "dependencies": { - "@superhero/core": "*" - }, - "devDependencies": { - "mocha": "5.1.0", - "mochawesome": "3.0.2", - "chai": "4.1.2", - "nyc": "11.7.1" - } -} -``` - -Our [`package.json`](https://docs.npmjs.com/files/package.json) file will dictate what dependencies we use. This example application will go through test cases, why we have a few `devDependencies` defined. - -#### `index.js` - -```js -const -CoreFactory = require('@superhero/core/factory'), -coreFactory = new CoreFactory, -core = coreFactory.create() - -core.add('api') -core.add('calculator') -core.add('logger') -core.add('http/server') - -core.load() - -core.locate('bootstrap').bootstrap().then(() => -core.locate('http/server').listen(process.env.HTTP_PORT)) -``` - -We start of by creating a core factory that will create the core object, central to our application. The core is designed to keep track of a global state related to it's context. You can create multiple cores if necessary, but normally it makes sens to only use one. This example will only use one core. - -The next step is to add services that we will use in relation to the core context. Here we add `api` and `calculator` that exists in the file tree previously defined. You also notice that we add the service `http/server` that does not exist in the file tree we defined. The `http/server` service is located in the core, but may not always be necessary to load depending on the scope of your application, so you need to add the `http/server` service when you want to set up an http server. -The framework will try to add services depending on a hierarchy of paths. - -- First it will try to load in relation to your `main script` -- next it attempts by dependency defined in your `package.json` file -- and finally it will attempt to load related to the core library of existing services. - -*This hierarchy allows you, as a developer, to overwrite anything in the framework with custom logic.* - -Next we load the core! This will eager load all the services previously added to the context. - -By bootstrapping the core, we run a few scripts that needs to run for some modules to function properly. As a developer you can hook into this process in the modules you write. - -And in the end, after we have added, loaded and bootstrapped the modules related to the context, we tell the server to listen to a specific port for network activity. - -### Api - -#### `src/api/config.js` - -```js -module.exports = -{ - http: - { - server: - { - routes: - { - 'create-calculation': - { - url : '/calculations', - method : 'post', - endpoint: 'api/endpoint/create-calculation', - view : 'http/server/view/json' - }, - 'authentication': - { - middleware : - [ - 'api/middleware/authentication' - ] - }, - 'append-calculation': - { - url : '/calculations/.+', - method : 'put', - endpoint: 'api/endpoint/append-calculation', - view : 'http/server/view/json', - dto : - { - 'id' : { 'url' : 2 }, - 'type' : { 'body' : 'type' }, - 'value' : { 'body' : 'value' } - } - } - } - } - }, - authentication: - { - apikey : 'ABC123456789' - } -} -``` - -I have here chosen to set up a folder structure with one module called `api`. This module will contain all the endpoints. As such, the config file of this module will specify the router setting for these endpoints. - -I set up two explicit routes: `create-calculation` and `append-calculation`, and one middleware route: `authentication`. - -- The **action** attribute declares on what **url pathname** the route will be valid for. -- The **method** attribute declares on what **url method** the route will be valid for - -The middleware route does not have an action or method constraint specified, so it's considered valid, but it does not have an endpoint specified; declaring it not unterminated. When a route is valid, but unterminated, then it will be merged together with every other valid route specified until one that is terminated appears, eg: one that has declared an endpoint. - -#### `src/api/endpoint/create-calculation.js` - -```js -const Dispatcher = require('@superhero/core/http/server/dispatcher') - -/** - * @extends {@superhero/core/http/server/dispatcher} - */ -class CreateCalculationEndpoint extends Dispatcher -{ - dispatch() - { - const - calculator = this.locator.locate('calculator'), - calculationId = calculator.createCalculation() - - this.view.body.id = calculationId - } -} - -module.exports = CreateCalculationEndpoint -``` - -The `create calculation` endpoint is here defined. - -- First we locate the `calculator` service. -- Next we create a calculation -- And finally we populate the `view model` with the returning calculation id - -***OBS! it's possible to define the `dispatch` method as `async`. The framework will `await` for the method to finish*** - -#### `src/api/endpoint/append-calculation.js` - -```js -const -Dispatcher = require('@superhero/core/http/server/dispatcher'), -PageNotFoundError = require('@superhero/core/http/server/dispatcher/error/page-not-found'), -BadRequestError = require('@superhero/core/http/server/dispatcher/error/bad-request') - -/** - * @extends {@superhero/core/http/server/dispatcher} - */ -class AppendCalculationEndpoint extends Dispatcher -{ - dispatch() - { - const - calculator = this.locator.locate('calculator'), - composer = this.locator.locate('composer'), - calculation = composer.compose('calculation', this.route.dto), - result = calculator.appendToCalculation(calculation) - - this.view.body.result = result - } - - onError(error) - { - switch(error) - { - case 'E_CALCULATION_COULD_NOT_BE_FOUND': - throw new PageNotFoundError('Calculation could not be found') - - case 'E_INVALID_CALCULATION_TYPE': - throw new BadRequestError(`Unrecognized type: "${this.route.dto.type}"`) - - case 'E_COMPOSER_INVALID_ATTRIBUTE': - throw new BadRequestError(error.message) - - default: - throw error - } - } -} - -module.exports = AppendCalculationEndpoint -``` - -The `append calculation` endpoint is here defined. - -- We start by showing you how to access data in the request through the `dto` *(Data Transfer Object)* located on the route. -- Next we locate the `calculator` service. -- Followed by appending a calculation to the stored calculated sum. -- And finally we populate the `view model` with the result of the calculation. - -Apart from the `dispatch` method, this time, we also define an `onError` method. This is the method that will be called if an error is thrown in the `dispatch` method. The first parameter to the `onError` method is the error that was thrown in the `dispatch` method. - -#### `src/api/middleware/authentication.js` - -```js -const -Dispatcher = require('@superhero/core/http/server/dispatcher'), -Unauthorized = require('@superhero/core/http/server/dispatcher/error/unauthorized') - -/** - * @extends {@superhero/core/http/server/dispatcher} - */ -class AuthenticationMiddleware extends Dispatcher -{ - async dispatch(next) - { - const - configuration = this.locator.locate('configuration'), - apikey = this.request.headers['api-key'] - - if(apikey === configuration.find('authentication.apikey')) - { - await next() - } - else - { - throw new Unauthorized('You are not authorized to access the requested resource') - } - } -} - -module.exports = AuthenticationMiddleware -``` - -This middleware is used for authentication. It's a simple implementation, one should look at using a more robust solution, involving an ACL, when creating a solution for production environment. This example serves a purpose to show a good use-case for when to apply a middleware, not how best practice regarding authentication is defined. - -**OBS!** The post handling (after the `next` callback has been called) will be handled in reversed order. - -``` - Middleware - ↓ ↑ - Middleware - ↓ ↑ - Endpoint -``` - -### Calculator - -#### `src/calculator/calculation.js` - -```js -/** - * @typedef {Object} CalculatorCalculationDto - * @property {number} id - * @property {string} type - * @property {number} value - */ -const dto = -{ - 'id': - { - 'type' : 'integer', - 'unsigned': true - }, - 'type': - { - 'type': 'string', - 'enum': - [ - 'addition', - 'subtraction' - ] - }, - 'value': - { - 'type': 'decimal' - } -} - -module.exports = dto -``` - -Defining a JSON schema for a dto; calculation. It's a good praxis to also define the type in "jsdoc", as seen above. - -A table over validation and filtration rules follows... - -| | boolean | decimal | integer | json | schema | string | timestamp | -|-------------|-----------|-----------|-----------|-----------|-----------|-----------|-----------| -| default | * | * | * | * | * | * | * | -| collection | boolean | boolean | boolean | boolean | boolean | boolean | boolean | -| optional | boolean | boolean | boolean | boolean | boolean | boolean | boolean | -| nullable | boolean | boolean | boolean | boolean | boolean | boolean | boolean | -| unsigned | - | boolean | boolean | - | - | - | - | -| min | - | number | number | - | - | number | timestamp | -| max | - | number | number | - | - | number | timestamp | -| gt | - | number | number | - | - | number | timestamp | -| lt | - | number | number | - | - | number | timestamp | -| enum | array | array | array | - | - | array | array | -| uppercase | - | - | - | - | - | boolean | - | -| lowercase | - | - | - | - | - | boolean | - | -| not-empty | - | - | - | - | - | boolean | - | -| stringified | - | - | - | boolean | - | - | - | -| indentation | - | - | - | number | - | - | - | -| schema | - | - | - | - | string | - | - | - -#### `src/calculator/config.js` - -```js -module.exports = -{ - composer: - { - schema: - { - 'calculation' : __dirname + '/calculation' - } - }, - locator: - { - 'calculator' : __dirname - } -} -``` - -In the `calculator config` we define where a path to a module is located, and where the locator can find a constituent locator to locate the service through. - -#### `src/calculator/index.js` - -```js -const -CalculationCouldNotBeFoundError = require('./error/calculation-could-not-be-found'), -InvalidCalculationTypeError = require('./error/invalid-calculation-type') - -/** - * Calculator service, manages calculations - */ -class Calculator -{ - /** - * @param {@superhero/eventbus} eventbus - */ - constructor(eventbus) - { - this.eventbus = eventbus - this.calculations = [] - } - - /** - * @returns {number} the id of the created calculation - */ - createCalculation() - { - const id = this.calculations.push(0) - this.eventbus.emit('calculator.calculation-created', { id }) - return id - } - - /** - * @throws {E_CALCULATION_COULD_NOT_BE_FOUND} - * @throws {E_INVALID_CALCULATION_TYPE} - * - * @param {CalculatorCalculation} dto - * - * @returns {number} the result of the calculation - */ - appendToCalculation({ id, type, value }) - { - if(id < 1 - || id > this.calculations.length) - { - throw new CalculationCouldNotBeFoundError(`Id out of range: "${id}/${this.calculations.length}"`) - } - - switch(type) - { - case 'addition': - { - const result = this.calculations[id - 1] += value - this.eventbus.emit('calculator.calculation-appended', { id, type, result }) - return result - } - case 'subtraction': - { - const result = this.calculations[id - 1] -= value - this.eventbus.emit('calculator.calculation-appended', { id, type, result }) - return result - } - default: - { - throw new InvalidCalculationTypeError(`Unrecognized type used for calculation: "${type}"`) - } - } - } -} - -module.exports = Calculator -``` - -The calculator service is here defined. Good practice dictate that we should define isolated errors for everything that can go wrong. On top of the service we require these errors to have access to the type and to be able to trow when needed. - -It's a simple service that creates a calculation and allows to append an additional calculation to an already created calculation. - -#### `src/calculator/locator.js` - -```js -const -Calculator = require('.'), -LocatorConstituent = require('@superhero/core/locator/constituent') - -/** - * @extends {@superhero/core/locator/constituent} - */ -class CalculatorLocator extends LocatorConstituent -{ - /** - * @returns {Calculator} - */ - locate() - { - const eventbus = this.locator.locate('eventbus') - return new Calculator(eventbus) - } -} - -module.exports = CalculatorLocator -``` - -The locator is responsible for dependency injection related to the service it creates. - -#### `src/calculator/error/calculation-could-not-be-found.js` - -```js -/** - * @extends {Error} - */ -class CalculationCouldNotBeFoundError extends Error -{ - constructor(...args) - { - super(...args) - this.code = 'E_CALCULATION_COULD_NOT_BE_FOUND' - } -} - -module.exports = CalculationCouldNotBeFoundError -``` - -A specific error with a specific error code; specifying what specific type of error is thrown. - -#### `src/calculator/error/invalid-calculation-type.js` - -```js -/** - * @extends {Error} - */ -class InvalidCalculationTypeError extends Error -{ - constructor(...args) - { - super(...args) - this.code = 'E_INVALID_CALCULATION_TYPE' - } -} - -module.exports = InvalidCalculationTypeError -``` - -Another specific error... - -### Logger - -#### `src/logger/config.js` - -```js -module.exports = -{ - eventbus: - { - observers: - { - 'calculator.calculation-created' : [ 'logger' ], - 'calculator.calculation-appended' : [ 'logger' ] - } - }, - locator: - { - 'logger' : __dirname - } -} -``` - -Attaching a logger observer to specific events. Notice that the `locator` describes where the service can be located from, then used in the `eventbus.observers` context. - -#### `src/logger/index.js` - -```js -/** - * @implements {@superhero/eventbus/observer} - */ -class Logger -{ - constructor(eventbus) - { - this.eventbus = eventbus - } - - observe(event) - { - this.eventbus.emit('logger.logged-event', event) - } -} - -module.exports = Logger -``` - -The logger observer simply implements an interface to be recognized as an observer by the `eventbus`. - -After we have logged the event to the console, we emit an event broadcasting that we have done so. By doing so, we can hook up to this event in the future if we like. For instance, when you like to create a test for the method, we can listen to this event. - -#### `src/logger/locator.js` - -```js -const -Logger = require('.'), -LocatorConstituent = require('@superhero/core/locator/constituent') - -/** - * @extends {@superhero/core/locator/constituent} - */ -class LoggerLocator extends LocatorConstituent -{ - /** - * @returns {Logger} - */ - locate() - { - const eventbus = this.locator.locate('eventbus') - return new Logger(eventbus) - } -} - -module.exports = LoggerLocator -``` - -The logger locator creates the logger for the `service locator`. - -### Test - -#### `test/mocha.opts` - -``` ---require test/init.js ---ui bdd ---full-trace ---timeout 5000 -``` - -There will probably be a lot of [settings you need to set for mocha](https://mochajs.org/api/mocha), sooner or later; just as well that we make it a praxis to define the options outside your `package.json` file. - -#### `test/init.js` - -```js -require.main.filename = __dirname + '/../src/index.js' -require.main.dirname = __dirname + '/../src' -``` - -The init script must set some variables for the core to function as expected in testing. - -The port is by design set through the environment variable `HTTP_PORT`. While testing we can set the variable to what ever we like, and what ever is suitable for the local machine we are on. - -#### `test/test.calculations.js` - -```js -describe('Calculations', () => -{ - const - expect = require('chai').expect, - context = require('mochawesome/addContext') - - let core - - before((done) => - { - const - CoreFactory = require('@superhero/core/factory'), - coreFactory = new CoreFactory - - core = coreFactory.create() - - core.add('api') - core.add('calculator') - core.add('logger') - core.add('http/server') - - core.load() - - core.locate('bootstrap').bootstrap().then(() => - { - core.locate('http/server').listen(9001) - core.locate('http/server').onListening(done) - }) - }) - - after(() => - { - core.locate('http/server').close() - }) - - it('A client can create a calculation', async function() - { - const configuration = core.locate('configuration') - const httpRequest = core.locate('http/request') - context(this, { title:'route', value:configuration.find('http.server.routes.create-calculation') }) - const response = await httpRequest.post('http://localhost:9001/calculations') - expect(response.data.id).to.be.equal(1) - }) - - it('A client can append a calculation to the result of a former calculation if authentication Api-Key', async function() - { - const configuration = core.locate('configuration') - const httpRequest = core.locate('http/request') - context(this, { title:'route', value:configuration.find('http.server.routes.append-calculation') }) - const url = 'http://localhost:9001/calculations/1' - const data = { id:1, type:'addition', value:100 } - const response_unauthorized = await httpRequest.put({ url, data }) - expect(response_unauthorized.status).to.be.equal(401) - const headers = { 'api-key':'ABC123456789' } - const response_authorized = await httpRequest.put({ headers, url, data }) - expect(response_authorized.data.result).to.be.equal(data.value) - }) -}) -``` - -#### `test/test.logger.js` - -```js -describe('Logger', () => -{ - const - expect = require('chai').expect, - context = require('mochawesome/addContext') - - let core - - before((done) => - { - const - CoreFactory = require('@superhero/core/factory'), - coreFactory = new CoreFactory - - core = coreFactory.create() - - core.add('api') - core.add('calculator') - core.add('logger') - core.add('http/server') - - core.load() - - core.locate('bootstrap').bootstrap().then(done) - }) - - it('Events are logged to the console', function(done) - { - const configuration = core.locate('configuration') - const eventbus = core.locate('eventbus') - context(this, { title:'observers', value:configuration.find('http.eventbus.observers') }) - eventbus.once('logger.logged-event', () => done()) - eventbus.emit('calculator.calculation-created', 'test') - }) -}) -``` - -Finally I have designed a few simple tests that proves the pattern, and gives insight to the expected interface. Here I suggest using a [BDD](https://en.wikipedia.org/wiki/Behavior-driven_development) approach. - -### Npm scripts - -#### To run the application: - -``` -npm install --production -npm start -``` - -#### To test the application: - -``` -npm install -npm test -``` - -#### For an auto-generated coverage report in html: - -``` -npm run docs-coverage -``` - -#### For an auto-generated test report in html: - -``` -npm run docs-tests -``` +# Core + +Licence: [MIT](https://opensource.org/licenses/MIT) + +--- + +- A core framework I use when developing in [nodejs](https://nodejs.org/en/docs/). +- I built the framework to help me build applications after a reactive [domain driven design (DDD)](https://en.wikipedia.org/wiki/Domain-driven_design) approch. +- The framework is designed to have little to no production dependencies. +- The framework offers solutions for different topics, not necessarily an http server. +- The vision of the framework is to offer a code structure to the developer that will help segregate responsibilities in projects through a [SOLID](https://en.wikipedia.org/wiki/SOLID) [OOP](https://en.wikipedia.org/wiki/Object-oriented_programming) approach. + +## Addons + +Consider extending the framework with one or multiple plugins where the extra functionality is required: +*Please note; you should use matching versions for core and the plugins by major and future releases* + +- [core-handlebars](http://github.com/superhero/js.core.handlebars) +- [core-resource](http://github.com/superhero/js.core.resource) +- [core-websocket](http://github.com/superhero/js.core.websocket) + +## Install + +`npm install @superhero/core` + +...or just set the dependency in your `package.json` file: + +```json +{ + "dependencies": + { + "@superhero/core": "*" + } +} +``` + +## Example Application + +An example application to get started, covering some recommended "best practices" regarding testing and docs generation. + +The application is a calculator with very basic functionality. The aim with this application is not to make a good calculator. The aim is to show you, who is reading this, how different parts of the framework works, or are intended to work. + +### Example Application › File structure + +``` +super-duper-app +├── docs +├── src +│ ├── api +│ │ ├── endpoint +│ │ │ ├── append-calculation.js +│ │ │ └── create-calculation.js +│ │ ├── middleware +│ │ │ └── authentication.js +│ │ └── config.js +│ ├── calculator +│ │ ├── error +│ │ | ├── calculation-could-not-be-found.js +│ │ | └── invalid-calculation-type.js +│ │ ├── calculation.js +│ │ ├── config.js +│ │ ├── index.js +│ │ └── locator.js +│ ├── logger +│ │ ├── config.js +│ │ ├── index.js +│ │ └── locator.js +│ └── index.js +├── test +│ ├── init.js +│ ├── mocha.opts +│ ├── test.calculations.js +│ └── test.logger.js +├── .gitignore +├── package.json +└── README.md +``` + +The file structure is here divided to only 3 modules + +- One called `api` *- that's responsible for the endpoints.* +- A second one called `calculator` *- that is responsible for domain logic.* +- The third one called `logger` *- an infrastructure layer that will log events to the console.* + +#### `.gitignore` + +``` +docs/generated +npm-debug.log +node_modules +package-lock.json +.nyc_output +``` + +Set up a `.gitignore` file to ignore some auto-generated files to keep a clean repository. + +#### `package.json` + +```js +{ + "name": "Super Duper App", + "version": "0.0.1", + "description": "An example application of the superhero/core framework", + "repository": "https://github.com/...", + "license": "MIT", + "main": "src/index.js", + "author": { + "name": "Padawan", + "email": "padawan@example.com" + }, + "scripts": { + "docs-coverage": "nyc mocha './test/test.*.js' --opts ./test/mocha.opts && nyc report --reporter=html --report-dir=./docs/generated/coverage", + "docs-tests": "mocha './test/test.*.js' --opts ./test/mocha.opts --reporter mochawesome --reporter-options reportDir=docs/generated/test,reportFilename=index,showHooks=always", + "test": "nyc mocha './test/test.*.js' --opts ./test/mocha.opts", + "start": "node ./src/index.js" + }, + "dependencies": { + "@superhero/core": "*" + }, + "devDependencies": { + "mocha": "5.1.0", + "mochawesome": "3.0.2", + "chai": "4.1.2", + "nyc": "11.7.1" + } +} +``` + +Our [`package.json`](https://docs.npmjs.com/files/package.json) file will dictate what dependencies we use. This example application will go through test cases, why we have a few `devDependencies` defined. + +#### `index.js` + +```js +const +CoreFactory = require('@superhero/core/factory'), +coreFactory = new CoreFactory, +core = coreFactory.create() + +core.add('api') +core.add('calculator') +core.add('logger') +core.add('http/server') + +core.load() + +core.locate('bootstrap').bootstrap().then(() => +core.locate('http/server').listen(process.env.HTTP_PORT)) +``` + +We start of by creating a core factory that will create the core object, central to our application. The core is designed to keep track of a global state related to it's context. You can create multiple cores if necessary, but normally it makes sens to only use one. This example will only use one core. + +The next step is to add services that we will use in relation to the core context. Here we add `api` and `calculator` that exists in the file tree previously defined. You also notice that we add the service `http/server` that does not exist in the file tree we defined. The `http/server` service is located in the core, but may not always be necessary to load depending on the scope of your application, so you need to add the `http/server` service when you want to set up an http server. +The framework will try to add services depending on a hierarchy of paths. + +- First it will try to load in relation to your `main script` +- next it attempts by dependency defined in your `package.json` file +- and finally it will attempt to load related to the core library of existing services. + +*This hierarchy allows you, as a developer, to overwrite anything in the framework with custom logic.* + +Next we load the core! This will eager load all the services previously added to the context. + +By bootstrapping the core, we run a few scripts that needs to run for some modules to function properly. As a developer you can hook into this process in the modules you write. + +And in the end, after we have added, loaded and bootstrapped the modules related to the context, we tell the server to listen to a specific port for network activity. + +### Api + +#### `src/api/config.js` + +```js +module.exports = +{ + http: + { + server: + { + routes: + { + 'create-calculation': + { + url : '/calculations', + method : 'post', + endpoint: 'api/endpoint/create-calculation', + view : 'http/server/view/json' + }, + 'authentication': + { + middleware : + [ + 'api/middleware/authentication' + ] + }, + 'append-calculation': + { + url : '/calculations/.+', + method : 'put', + endpoint: 'api/endpoint/append-calculation', + view : 'http/server/view/json', + dto : + { + 'id' : { 'url' : 2 }, + 'type' : { 'body' : 'type' }, + 'value' : { 'body' : 'value' } + } + } + } + } + }, + authentication: + { + apikey : 'ABC123456789' + } +} +``` + +I have here chosen to set up a folder structure with one module called `api`. This module will contain all the endpoints. As such, the config file of this module will specify the router setting for these endpoints. + +I set up two explicit routes: `create-calculation` and `append-calculation`, and one middleware route: `authentication`. + +- The **action** attribute declares on what **url pathname** the route will be valid for. +- The **method** attribute declares on what **url method** the route will be valid for + +The middleware route does not have an action or method constraint specified, so it's considered valid, but it does not have an endpoint specified; declaring it not unterminated. When a route is valid, but unterminated, then it will be merged together with every other valid route specified until one that is terminated appears, eg: one that has declared an endpoint. + +#### `src/api/endpoint/create-calculation.js` + +```js +const Dispatcher = require('@superhero/core/http/server/dispatcher') + +/** + * @extends {@superhero/core/http/server/dispatcher} + */ +class CreateCalculationEndpoint extends Dispatcher +{ + dispatch() + { + const + calculator = this.locator.locate('calculator'), + calculationId = calculator.createCalculation() + + this.view.body.id = calculationId + } +} + +module.exports = CreateCalculationEndpoint +``` + +The `create calculation` endpoint is here defined. + +- First we locate the `calculator` service. +- Next we create a calculation +- And finally we populate the `view model` with the returning calculation id + +***OBS! it's possible to define the `dispatch` method as `async`. The framework will `await` for the method to finish*** + +#### `src/api/endpoint/append-calculation.js` + +```js +const +Dispatcher = require('@superhero/core/http/server/dispatcher'), +PageNotFoundError = require('@superhero/core/http/server/dispatcher/error/page-not-found'), +BadRequestError = require('@superhero/core/http/server/dispatcher/error/bad-request') + +/** + * @extends {@superhero/core/http/server/dispatcher} + */ +class AppendCalculationEndpoint extends Dispatcher +{ + dispatch() + { + const + calculator = this.locator.locate('calculator'), + composer = this.locator.locate('composer'), + calculation = composer.compose('calculation', this.route.dto), + result = calculator.appendToCalculation(calculation) + + this.view.body.result = result + } + + onError(error) + { + switch(error) + { + case 'E_CALCULATION_COULD_NOT_BE_FOUND': + throw new PageNotFoundError('Calculation could not be found') + + case 'E_INVALID_CALCULATION_TYPE': + throw new BadRequestError(`Unrecognized type: "${this.route.dto.type}"`) + + case 'E_COMPOSER_INVALID_ATTRIBUTE': + throw new BadRequestError(error.message) + + default: + throw error + } + } +} + +module.exports = AppendCalculationEndpoint +``` + +The `append calculation` endpoint is here defined. + +- We start by showing you how to access data in the request through the `dto` *(Data Transfer Object)* located on the route. +- Next we locate the `calculator` service. +- Followed by appending a calculation to the stored calculated sum. +- And finally we populate the `view model` with the result of the calculation. + +Apart from the `dispatch` method, this time, we also define an `onError` method. This is the method that will be called if an error is thrown in the `dispatch` method. The first parameter to the `onError` method is the error that was thrown in the `dispatch` method. + +#### `src/api/middleware/authentication.js` + +```js +const +Dispatcher = require('@superhero/core/http/server/dispatcher'), +Unauthorized = require('@superhero/core/http/server/dispatcher/error/unauthorized') + +/** + * @extends {@superhero/core/http/server/dispatcher} + */ +class AuthenticationMiddleware extends Dispatcher +{ + async dispatch(next) + { + const + configuration = this.locator.locate('configuration'), + apikey = this.request.headers['api-key'] + + if(apikey === configuration.find('authentication.apikey')) + { + await next() + } + else + { + throw new Unauthorized('You are not authorized to access the requested resource') + } + } +} + +module.exports = AuthenticationMiddleware +``` + +This middleware is used for authentication. It's a simple implementation, one should look at using a more robust solution, involving an ACL, when creating a solution for production environment. This example serves a purpose to show a good use-case for when to apply a middleware, not how best practice regarding authentication is defined. + +**OBS!** The post handling (after the `next` callback has been called) will be handled in reversed order. + +``` + Middleware + ↓ ↑ + Middleware + ↓ ↑ + Endpoint +``` + +### Calculator + +#### `src/calculator/calculation.js` + +```js +/** + * @typedef {Object} CalculatorCalculationDto + * @property {number} id + * @property {string} type + * @property {number} value + */ +const dto = +{ + 'id': + { + 'type' : 'integer', + 'unsigned': true + }, + 'type': + { + 'type': 'string', + 'enum': + [ + 'addition', + 'subtraction' + ] + }, + 'value': + { + 'type': 'decimal' + } +} + +module.exports = dto +``` + +Defining a JSON schema for a dto; calculation. It's a good praxis to also define the type in "jsdoc", as seen above. + +A table over validation and filtration rules follows... + +| | boolean | decimal | integer | json | schema | string | timestamp | +|-------------|-----------|-----------|-----------|-----------|-----------|-----------|-----------| +| default | * | * | * | * | * | * | * | +| collection | boolean | boolean | boolean | boolean | boolean | boolean | boolean | +| optional | boolean | boolean | boolean | boolean | boolean | boolean | boolean | +| nullable | boolean | boolean | boolean | boolean | boolean | boolean | boolean | +| unsigned | - | boolean | boolean | - | - | - | - | +| min | - | number | number | - | - | number | timestamp | +| max | - | number | number | - | - | number | timestamp | +| gt | - | number | number | - | - | number | timestamp | +| lt | - | number | number | - | - | number | timestamp | +| enum | array | array | array | - | - | array | array | +| uppercase | - | - | - | - | - | boolean | - | +| lowercase | - | - | - | - | - | boolean | - | +| not-empty | - | - | - | - | - | boolean | - | +| stringified | - | - | - | boolean | - | - | - | +| indentation | - | - | - | number | - | - | - | +| schema | - | - | - | - | string | - | - | + +#### `src/calculator/config.js` + +```js +module.exports = +{ + composer: + { + schema: + { + 'calculation' : __dirname + '/calculation' + } + }, + locator: + { + 'calculator' : __dirname + } +} +``` + +In the `calculator config` we define where a path to a module is located, and where the locator can find a constituent locator to locate the service through. + +#### `src/calculator/index.js` + +```js +const +CalculationCouldNotBeFoundError = require('./error/calculation-could-not-be-found'), +InvalidCalculationTypeError = require('./error/invalid-calculation-type') + +/** + * Calculator service, manages calculations + */ +class Calculator +{ + /** + * @param {@superhero/eventbus} eventbus + */ + constructor(eventbus) + { + this.eventbus = eventbus + this.calculations = [] + } + + /** + * @returns {number} the id of the created calculation + */ + createCalculation() + { + const id = this.calculations.push(0) + this.eventbus.emit('calculator.calculation-created', { id }) + return id + } + + /** + * @throws {E_CALCULATION_COULD_NOT_BE_FOUND} + * @throws {E_INVALID_CALCULATION_TYPE} + * + * @param {CalculatorCalculation} dto + * + * @returns {number} the result of the calculation + */ + appendToCalculation({ id, type, value }) + { + if(id < 1 + || id > this.calculations.length) + { + throw new CalculationCouldNotBeFoundError(`Id out of range: "${id}/${this.calculations.length}"`) + } + + switch(type) + { + case 'addition': + { + const result = this.calculations[id - 1] += value + this.eventbus.emit('calculator.calculation-appended', { id, type, result }) + return result + } + case 'subtraction': + { + const result = this.calculations[id - 1] -= value + this.eventbus.emit('calculator.calculation-appended', { id, type, result }) + return result + } + default: + { + throw new InvalidCalculationTypeError(`Unrecognized type used for calculation: "${type}"`) + } + } + } +} + +module.exports = Calculator +``` + +The calculator service is here defined. Good practice dictate that we should define isolated errors for everything that can go wrong. On top of the service we require these errors to have access to the type and to be able to trow when needed. + +It's a simple service that creates a calculation and allows to append an additional calculation to an already created calculation. + +#### `src/calculator/locator.js` + +```js +const +Calculator = require('.'), +LocatorConstituent = require('@superhero/core/locator/constituent') + +/** + * @extends {@superhero/core/locator/constituent} + */ +class CalculatorLocator extends LocatorConstituent +{ + /** + * @returns {Calculator} + */ + locate() + { + const eventbus = this.locator.locate('eventbus') + return new Calculator(eventbus) + } +} + +module.exports = CalculatorLocator +``` + +The locator is responsible for dependency injection related to the service it creates. + +#### `src/calculator/error/calculation-could-not-be-found.js` + +```js +/** + * @extends {Error} + */ +class CalculationCouldNotBeFoundError extends Error +{ + constructor(...args) + { + super(...args) + this.code = 'E_CALCULATION_COULD_NOT_BE_FOUND' + } +} + +module.exports = CalculationCouldNotBeFoundError +``` + +A specific error with a specific error code; specifying what specific type of error is thrown. + +#### `src/calculator/error/invalid-calculation-type.js` + +```js +/** + * @extends {Error} + */ +class InvalidCalculationTypeError extends Error +{ + constructor(...args) + { + super(...args) + this.code = 'E_INVALID_CALCULATION_TYPE' + } +} + +module.exports = InvalidCalculationTypeError +``` + +Another specific error... + +### Logger + +#### `src/logger/config.js` + +```js +module.exports = +{ + eventbus: + { + observers: + { + 'calculator.calculation-created' : [ 'logger' ], + 'calculator.calculation-appended' : [ 'logger' ] + } + }, + locator: + { + 'logger' : __dirname + } +} +``` + +Attaching a logger observer to specific events. Notice that the `locator` describes where the service can be located from, then used in the `eventbus.observers` context. + +#### `src/logger/index.js` + +```js +/** + * @implements {@superhero/eventbus/observer} + */ +class Logger +{ + constructor(eventbus) + { + this.eventbus = eventbus + } + + observe(event) + { + this.eventbus.emit('logger.logged-event', event) + } +} + +module.exports = Logger +``` + +The logger observer simply implements an interface to be recognized as an observer by the `eventbus`. + +After we have logged the event to the console, we emit an event broadcasting that we have done so. By doing so, we can hook up to this event in the future if we like. For instance, when you like to create a test for the method, we can listen to this event. + +#### `src/logger/locator.js` + +```js +const +Logger = require('.'), +LocatorConstituent = require('@superhero/core/locator/constituent') + +/** + * @extends {@superhero/core/locator/constituent} + */ +class LoggerLocator extends LocatorConstituent +{ + /** + * @returns {Logger} + */ + locate() + { + const eventbus = this.locator.locate('eventbus') + return new Logger(eventbus) + } +} + +module.exports = LoggerLocator +``` + +The logger locator creates the logger for the `service locator`. + +### Test + +#### `test/mocha.opts` + +``` +--require test/init.js +--ui bdd +--full-trace +--timeout 5000 +``` + +There will probably be a lot of [settings you need to set for mocha](https://mochajs.org/api/mocha), sooner or later; just as well that we make it a praxis to define the options outside your `package.json` file. + +#### `test/init.js` + +```js +require.main.filename = __dirname + '/../src/index.js' +require.main.dirname = __dirname + '/../src' +``` + +The init script must set some variables for the core to function as expected in testing. + +The port is by design set through the environment variable `HTTP_PORT`. While testing we can set the variable to what ever we like, and what ever is suitable for the local machine we are on. + +#### `test/test.calculations.js` + +```js +describe('Calculations', () => +{ + const + expect = require('chai').expect, + context = require('mochawesome/addContext') + + let core + + before((done) => + { + const + CoreFactory = require('@superhero/core/factory'), + coreFactory = new CoreFactory + + core = coreFactory.create() + + core.add('api') + core.add('calculator') + core.add('logger') + core.add('http/server') + + core.load() + + core.locate('bootstrap').bootstrap().then(() => + { + core.locate('http/server').listen(9001) + core.locate('http/server').onListening(done) + }) + }) + + after(() => + { + core.locate('http/server').close() + }) + + it('A client can create a calculation', async function() + { + const configuration = core.locate('configuration') + const httpRequest = core.locate('http/request') + context(this, { title:'route', value:configuration.find('http.server.routes.create-calculation') }) + const response = await httpRequest.post('http://localhost:9001/calculations') + expect(response.data.id).to.be.equal(1) + }) + + it('A client can append a calculation to the result of a former calculation if authentication Api-Key', async function() + { + const configuration = core.locate('configuration') + const httpRequest = core.locate('http/request') + context(this, { title:'route', value:configuration.find('http.server.routes.append-calculation') }) + const url = 'http://localhost:9001/calculations/1' + const data = { id:1, type:'addition', value:100 } + const response_unauthorized = await httpRequest.put({ url, data }) + expect(response_unauthorized.status).to.be.equal(401) + const headers = { 'api-key':'ABC123456789' } + const response_authorized = await httpRequest.put({ headers, url, data }) + expect(response_authorized.data.result).to.be.equal(data.value) + }) +}) +``` + +#### `test/test.logger.js` + +```js +describe('Logger', () => +{ + const + expect = require('chai').expect, + context = require('mochawesome/addContext') + + let core + + before((done) => + { + const + CoreFactory = require('@superhero/core/factory'), + coreFactory = new CoreFactory + + core = coreFactory.create() + + core.add('api') + core.add('calculator') + core.add('logger') + core.add('http/server') + + core.load() + + core.locate('bootstrap').bootstrap().then(done) + }) + + it('Events are logged to the console', function(done) + { + const configuration = core.locate('configuration') + const eventbus = core.locate('eventbus') + context(this, { title:'observers', value:configuration.find('http.eventbus.observers') }) + eventbus.once('logger.logged-event', () => done()) + eventbus.emit('calculator.calculation-created', 'test') + }) +}) +``` + +Finally I have designed a few simple tests that proves the pattern, and gives insight to the expected interface. Here I suggest using a [BDD](https://en.wikipedia.org/wiki/Behavior-driven_development) approach. + +### Npm scripts + +#### To run the application: + +``` +npm install --production +npm start +``` + +#### To test the application: + +``` +npm install +npm test +``` + +#### For an auto-generated coverage report in html: + +``` +npm run docs-coverage +``` + +#### For an auto-generated test report in html: + +``` +npm run docs-tests +``` diff --git a/doc/book/1-core-components/bootstrap.md b/doc/book/1-core-components/bootstrap.md index 44de957..9a451f6 100644 --- a/doc/book/1-core-components/bootstrap.md +++ b/doc/book/1-core-components/bootstrap.md @@ -1,47 +1,47 @@ -# Bootstrap - -The bootstrap component is meant to define a post core load, and pre application execution, process. - -The bootstrap component is important that you run, as there are framework specific logic that depends on that the process has finished. - ---- - -The process is a simple concept. In the configuration file of any component that you added and loaded to the core, you can specify a configuration that specifies what operation should run on bootstrap. - -```js -{ - core: - { - bootstrap: - { - 'unique-key' : 'foo/bar' - }, - locator: - { - 'foo/bar' : '/absolute/path/to/service' - } - } -} -``` - -The example above is a snippet that describes how you can hook into the process by specifying a bootstrap service. The bootstrap process will use the `core/locator` component to locate the bootstrap service. Therefor you must first specify the directory path to the service for the locator as described in the chapter that documents the `core/locator` component. - -As the example above shows, you must specify a key for the bootstrap operation, a unique name that identifies the operation. In this way, by design, you are able to replace or deactivate the bootstrap operation. You can deactivate the operation by replacing the value with a false boolean value. - ---- - -## The bootstrap contract - -The bootstrap service must honor the bootstrap contract, which simply dictates that a bootstrap method is present in the interface of the service. No arguments are passed to the method. The method can be asynchronous. Example of a bootstrap service follows below. - -```js -class Foobar -{ - bootstrap() - { - console.log('Hello world') - } -} -``` - -In the example above, the bootstrap method is not prefixed with the `async` keyword. The bootstrap manager will however `await` the result of every bootstrap operation. That means that the bootstrap process is a serial asynchronous process, composed of several bootstrap operations that are loaded in a hierarchical order. The hierarchical order is defined by the order which the components are added to the core component. +# Bootstrap + +The bootstrap component is meant to define a post core load, and pre application execution, process. + +The bootstrap component is important that you run, as there are framework specific logic that depends on that the process has finished. + +--- + +The process is a simple concept. In the configuration file of any component that you added and loaded to the core, you can specify a configuration that specifies what operation should run on bootstrap. + +```js +{ + core: + { + bootstrap: + { + 'unique-key' : 'foo/bar' + }, + locator: + { + 'foo/bar' : '/absolute/path/to/service' + } + } +} +``` + +The example above is a snippet that describes how you can hook into the process by specifying a bootstrap service. The bootstrap process will use the `core/locator` component to locate the bootstrap service. Therefor you must first specify the directory path to the service for the locator as described in the chapter that documents the `core/locator` component. + +As the example above shows, you must specify a key for the bootstrap operation, a unique name that identifies the operation. In this way, by design, you are able to replace or deactivate the bootstrap operation. You can deactivate the operation by replacing the value with a false boolean value. + +--- + +## The bootstrap contract + +The bootstrap service must honor the bootstrap contract, which simply dictates that a bootstrap method is present in the interface of the service. No arguments are passed to the method. The method can be asynchronous. Example of a bootstrap service follows below. + +```js +class Foobar +{ + bootstrap() + { + console.log('Hello world') + } +} +``` + +In the example above, the bootstrap method is not prefixed with the `async` keyword. The bootstrap manager will however `await` the result of every bootstrap operation. That means that the bootstrap process is a serial asynchronous process, composed of several bootstrap operations that are loaded in a hierarchical order. The hierarchical order is defined by the order which the components are added to the core component. diff --git a/doc/book/1-core-components/cli.md b/doc/book/1-core-components/cli.md index 0b6ace6..e09a5b3 100644 --- a/doc/book/1-core-components/cli.md +++ b/doc/book/1-core-components/cli.md @@ -1,91 +1,91 @@ -# CLI - -The `core/cli` component offers the ability to write a command line interface application. The component is for you who want to write an application that can be interacted with through a command line interface in the terminal. - ---- - -## Write - -```js -const cli = core.locate('core/cli') -cli.write('Hello World') -``` - -The above example will have the following output. - -``` -Hello World -``` - ---- - -## Output in color - -You can also specify a color of the output by passing an additional argument to the write method. - -```js -cli.write('Hello World', 'blue') -``` - -The above example will have the following output, but in blue color... - -``` -Hello World -``` - -The possible color values follows below. - -- black -- blue -- cyan -- green -- magenta -- red -- yellow -- white - ---- - -## Question - -It is possible for you to ask for input from the client. You do so by asking a question. - -```js -const answer = await cli.question('What is the meaning of life?') -``` - -The expected output is the same as previous examples. - -``` -What is the meaning of life? -``` - -The process will wait for the client to write an input, followed by the enter button that will return the value to the `answer` variable. The method is asynchronous. - ---- - -## Autocomplete - -If you like to narrow down the availible answers you can pass along an array of acceptable answers. - -```js -const answer = await cli.question('Keep it real?', ['yes', 'no']) -``` - -With the alternatives defined, you can write the beginning of an accepted answer followed by hitting the tab key, then the line will be auto completed with the complete answer. - -As the example below shows, if the answer provided is not acceptable, alternatives will be written to the output, followed by the same question again. - -``` -Keep it real? maybe -You must chose one of the alternatives yes or no -Keep it real? -``` - -It is also possible to see all accepteable answers by double tapping the tabb key, expected output follows below. - -``` -Keep it real? -yes no -Keep it real? -``` +# CLI + +The `core/cli` component offers the ability to write a command line interface application. The component is for you who want to write an application that can be interacted with through a command line interface in the terminal. + +--- + +## Write + +```js +const cli = core.locate('core/cli') +cli.write('Hello World') +``` + +The above example will have the following output. + +``` +Hello World +``` + +--- + +## Output in color + +You can also specify a color of the output by passing an additional argument to the write method. + +```js +cli.write('Hello World', 'blue') +``` + +The above example will have the following output, but in blue color... + +``` +Hello World +``` + +The possible color values follows below. + +- black +- blue +- cyan +- green +- magenta +- red +- yellow +- white + +--- + +## Question + +It is possible for you to ask for input from the client. You do so by asking a question. + +```js +const answer = await cli.question('What is the meaning of life?') +``` + +The expected output is the same as previous examples. + +``` +What is the meaning of life? +``` + +The process will wait for the client to write an input, followed by the enter button that will return the value to the `answer` variable. The method is asynchronous. + +--- + +## Autocomplete + +If you like to narrow down the availible answers you can pass along an array of acceptable answers. + +```js +const answer = await cli.question('Keep it real?', ['yes', 'no']) +``` + +With the alternatives defined, you can write the beginning of an accepted answer followed by hitting the tab key, then the line will be auto completed with the complete answer. + +As the example below shows, if the answer provided is not acceptable, alternatives will be written to the output, followed by the same question again. + +``` +Keep it real? maybe +You must chose one of the alternatives yes or no +Keep it real? +``` + +It is also possible to see all accepteable answers by double tapping the tabb key, expected output follows below. + +``` +Keep it real? +yes no +Keep it real? +``` diff --git a/doc/book/1-core-components/configuration.md b/doc/book/1-core-components/configuration.md index 2b82e08..90632dc 100644 --- a/doc/book/1-core-components/configuration.md +++ b/doc/book/1-core-components/configuration.md @@ -1,55 +1,55 @@ -# Configuration - -The configuration component, in the context of the framework, is perhaps not to unexpectedly the configuration manager, the component that contains the defined configuration data. - -When the core component loads all added components in your main script, the core merges all configuration files of all components together and stores them in this confifiguration component. Post loading, the configuration data is frozen, no further merge or alteration to the data can be made post the core load process. - ---- - -## The find query - -The configuration component has a very simple interface, basicly only one method that is relevant; the `find` method. The find method purpose is to find a configuration variable in the tree of defined configurations given a query as argument. - -In the examples below, it is assumed that the configurations that has been loaded has been loaded with a defined json that looks as the following example shows. - -```js -{ - foo: - { - bar: - { - baz: 'qux' - } - } -} -``` - -If we like to find the leaf configuration variable `baz`, we can use the query `foo.bar.baz`. - -```js -const -configuration = core.locate('core/configuration'), -bazConfig = configuration.find('foo.bar.baz') -``` - -### Alternative find query - -Alternatively, we could instead use a query that seperates the path by a forward slash `/`. For example, instead of using the query `foo.bar.baz`, we can use the query `foo/bar/baz`, and expect the same result; the variable `bazConfig` will in both cases be of the value `qux`. - ---- - -## The fallback - -The purpose of the find method is to offer a solution to the problem presented in javascript working with nested objects. If you try to access a reference that does not exist, then an error will be thrown, as the example below shows. - -```js -const config = configuration.config.none.existing.path -``` - -Above example will throw: `Uncaught TypeError: Cannot read property 'existing' of undefined`, while the following example below will instead return an `undefined` value. - -```js -const config = configuration.find('none.existing.path') -``` - -Both alternatives precented above is a valid approch. If you expect an error to be thrown when attempting to access a configuration path that does not exist, then you should not use the `find` method. The only difference between the 2 alternatives is the fallback to `undefined`. +# Configuration + +The configuration component, in the context of the framework, is perhaps not to unexpectedly the configuration manager, the component that contains the defined configuration data. + +When the core component loads all added components in your main script, the core merges all configuration files of all components together and stores them in this confifiguration component. Post loading, the configuration data is frozen, no further merge or alteration to the data can be made post the core load process. + +--- + +## The find query + +The configuration component has a very simple interface, basicly only one method that is relevant; the `find` method. The find method purpose is to find a configuration variable in the tree of defined configurations given a query as argument. + +In the examples below, it is assumed that the configurations that has been loaded has been loaded with a defined json that looks as the following example shows. + +```js +{ + foo: + { + bar: + { + baz: 'qux' + } + } +} +``` + +If we like to find the leaf configuration variable `baz`, we can use the query `foo.bar.baz`. + +```js +const +configuration = core.locate('core/configuration'), +bazConfig = configuration.find('foo.bar.baz') +``` + +### Alternative find query + +Alternatively, we could instead use a query that seperates the path by a forward slash `/`. For example, instead of using the query `foo.bar.baz`, we can use the query `foo/bar/baz`, and expect the same result; the variable `bazConfig` will in both cases be of the value `qux`. + +--- + +## The fallback + +The purpose of the find method is to offer a solution to the problem presented in javascript working with nested objects. If you try to access a reference that does not exist, then an error will be thrown, as the example below shows. + +```js +const config = configuration.config.none.existing.path +``` + +Above example will throw: `Uncaught TypeError: Cannot read property 'existing' of undefined`, while the following example below will instead return an `undefined` value. + +```js +const config = configuration.find('none.existing.path') +``` + +Both alternatives precented above is a valid approch. If you expect an error to be thrown when attempting to access a configuration path that does not exist, then you should not use the `find` method. The only difference between the 2 alternatives is the fallback to `undefined`. diff --git a/doc/book/1-core-components/console.md b/doc/book/1-core-components/console.md index 2449e7b..a7479ee 100644 --- a/doc/book/1-core-components/console.md +++ b/doc/book/1-core-components/console.md @@ -1,64 +1,64 @@ -# Console - -The console component is an extension of the `@superhero/debug` module at npm. Go check out the documentation for that module to find out more specificly the current possible settings and abilities to work with this component. If I where to write the full documentations here as well, I will have to maintan 2 different sets of documentations. - -In this documentation you will however learn simple behaviour, how to integrate with the component through the configuration file and what the altered default behaviour from the extended module is. - ---- - -## Simple behaviour - -The purpose of the component is to log a message, by default to the `stdout` and `stderr`, but they can both be configured to any writable stream. - -```js -core.locate('core/console').log('Hello World') -``` - -Above example will ouput `2019-09-02 13:26:41 Hello World`. As you can see, a prefixed timestamp is added by default. - ---- - -## Configure the component - -The configuration structure can be altered by writing over the namespace `core.console` in the configuration settings, defined in one of the aded components you add to your application. - -```js -{ - core: - { - console: - { - color : false, - maxArrayLength : 10, - maxObjectDepth : 10, - maxStringLength : false - } - } -} -``` - -Above example shows a configuration that alters the default settings. `color` is set to false toturn of coloring of the output. `maxArrayLength` and `maxObjectDepth` determines how many items of an array or object will be printed. `maxStringLength` set to false will turn of the behaviour that by default truncates a string to set length. - -There are more settings then this that you can play around with, see the documention of the `@superhero/debug` module to learn more. - ---- - -## Altered default behaviour - -The difference between the `@superhero/debug` module at npm and the component in this framework, is the color and prefix that will be used when using the methods `error` and `warning`. - -```js -const console = core.locate('core/console') - -console.warning('Something strange has happend') -console.error('Meltdown!') -``` - -Above example will output the following to the terminal. - -``` -2019-09-02 13:26:41 Warning: Something strange has happend -2019-09-02 13:26:41 Error: Meltdown! -``` - -The above output will be in yellow for warning, and in red for error. +# Console + +The console component is an extension of the `@superhero/debug` module at npm. Go check out the documentation for that module to find out more specificly the current possible settings and abilities to work with this component. If I where to write the full documentations here as well, I will have to maintan 2 different sets of documentations. + +In this documentation you will however learn simple behaviour, how to integrate with the component through the configuration file and what the altered default behaviour from the extended module is. + +--- + +## Simple behaviour + +The purpose of the component is to log a message, by default to the `stdout` and `stderr`, but they can both be configured to any writable stream. + +```js +core.locate('core/console').log('Hello World') +``` + +Above example will ouput `2019-09-02 13:26:41 Hello World`. As you can see, a prefixed timestamp is added by default. + +--- + +## Configure the component + +The configuration structure can be altered by writing over the namespace `core.console` in the configuration settings, defined in one of the aded components you add to your application. + +```js +{ + core: + { + console: + { + color : false, + maxArrayLength : 10, + maxObjectDepth : 10, + maxStringLength : false + } + } +} +``` + +Above example shows a configuration that alters the default settings. `color` is set to false toturn of coloring of the output. `maxArrayLength` and `maxObjectDepth` determines how many items of an array or object will be printed. `maxStringLength` set to false will turn of the behaviour that by default truncates a string to set length. + +There are more settings then this that you can play around with, see the documention of the `@superhero/debug` module to learn more. + +--- + +## Altered default behaviour + +The difference between the `@superhero/debug` module at npm and the component in this framework, is the color and prefix that will be used when using the methods `error` and `warning`. + +```js +const console = core.locate('core/console') + +console.warning('Something strange has happend') +console.error('Meltdown!') +``` + +Above example will output the following to the terminal. + +``` +2019-09-02 13:26:41 Warning: Something strange has happend +2019-09-02 13:26:41 Error: Meltdown! +``` + +The above output will be in yellow for warning, and in red for error. diff --git a/doc/book/1-core-components/deepclone.md b/doc/book/1-core-components/deepclone.md index bbd05b3..1f505cb 100644 --- a/doc/book/1-core-components/deepclone.md +++ b/doc/book/1-core-components/deepclone.md @@ -1,19 +1,19 @@ -# Deepclone - -The purpose of the component is to clone an object and all its ancestor. The interface is as the example below shows. - -```js -const -foobar = { foo:{ bar:'baz' } }, -deepclone = core.locate('core/deepclone'), -foobarClone = deepclone.clone(foobar) - -foobar.foo.bar === foobarClone.foo.bar -// > true - -foobar.foo.bar = 'qux' -foobar.foo.bar === foobarClone.foo.bar -// > false -``` - -The above example proves that the nested ancestor `foo.bar` in the clone, is not a reference to the same memory space. +# Deepclone + +The purpose of the component is to clone an object and all its ancestor. The interface is as the example below shows. + +```js +const +foobar = { foo:{ bar:'baz' } }, +deepclone = core.locate('core/deepclone'), +foobarClone = deepclone.clone(foobar) + +foobar.foo.bar === foobarClone.foo.bar +// > true + +foobar.foo.bar = 'qux' +foobar.foo.bar === foobarClone.foo.bar +// > false +``` + +The above example proves that the nested ancestor `foo.bar` in the clone, is not a reference to the same memory space. diff --git a/doc/book/1-core-components/deepfind.md b/doc/book/1-core-components/deepfind.md index dcab33a..f24333f 100644 --- a/doc/book/1-core-components/deepfind.md +++ b/doc/book/1-core-components/deepfind.md @@ -1,20 +1,20 @@ -# Deepfind - -The purpose of this component is to offer the ability to find a branch or a leaf in an object by specifed query. If you specify an incorrect query, you will be returned an undefined value. - -```js -const -foobar = { foo:{ bar:'baz' } }, -deepfind = core.locate('core/deepfind') - -deepfind.find('foo.bar', foobar) -// > 'baz' - -deepfind.find('foo/bar', foobar) -// > 'baz' - -deepfind.find('none.existing.path', foobar) -// > undefined -``` - -As the above example shows, you can use different delimiters - *a dot `.` or a forward slash `/`* - to seperate the path segments in the query with the same result. +# Deepfind + +The purpose of this component is to offer the ability to find a branch or a leaf in an object by specifed query. If you specify an incorrect query, you will be returned an undefined value. + +```js +const +foobar = { foo:{ bar:'baz' } }, +deepfind = core.locate('core/deepfind') + +deepfind.find('foo.bar', foobar) +// > 'baz' + +deepfind.find('foo/bar', foobar) +// > 'baz' + +deepfind.find('none.existing.path', foobar) +// > undefined +``` + +As the above example shows, you can use different delimiters - *a dot `.` or a forward slash `/`* - to seperate the path segments in the query with the same result. diff --git a/doc/book/1-core-components/deepfreeze.md b/doc/book/1-core-components/deepfreeze.md index 439d0ae..2b14215 100644 --- a/doc/book/1-core-components/deepfreeze.md +++ b/doc/book/1-core-components/deepfreeze.md @@ -1,20 +1,20 @@ -# Deepfreeze - -This components purpose is to freeze an object, and all nested ansestors, that is passed to the method `freeze`. The difference between this component and the normal JavaScript implementation; `Object.freeze(obj)`, is that the `core/deepfreeze` component will freeze every nested object, where `Object.freeze(obj)` will only freeze the passed reference and none of its ansestors. - -```js -const -foobar = { foo:{ bar:'baz' } }, -deepfreeze = core.locate('core/deepfreeze') - -deepfreeze.freeze(foobar) - -foobar.foo.bar = 'qux' - -foobar.foo.bar === 'qux' -// > false -foobar.foo.bar === 'baz' -// > true -``` - -As the example above shows, when an object is frozen, it is rendered imutable, and the value can no longer be altered. +# Deepfreeze + +This components purpose is to freeze an object, and all nested ansestors, that is passed to the method `freeze`. The difference between this component and the normal JavaScript implementation; `Object.freeze(obj)`, is that the `core/deepfreeze` component will freeze every nested object, where `Object.freeze(obj)` will only freeze the passed reference and none of its ansestors. + +```js +const +foobar = { foo:{ bar:'baz' } }, +deepfreeze = core.locate('core/deepfreeze') + +deepfreeze.freeze(foobar) + +foobar.foo.bar = 'qux' + +foobar.foo.bar === 'qux' +// > false +foobar.foo.bar === 'baz' +// > true +``` + +As the example above shows, when an object is frozen, it is rendered imutable, and the value can no longer be altered. diff --git a/doc/book/1-core-components/deepmerge.md b/doc/book/1-core-components/deepmerge.md index 01fc3a6..cdf70d6 100644 --- a/doc/book/1-core-components/deepmerge.md +++ b/doc/book/1-core-components/deepmerge.md @@ -1,110 +1,110 @@ -# Deepmerge - -This component addresses the problem to merge a nested, and sometimes complicated, object structure, with one ore many others objects. - -```js -const -obj1 = { foo:{ bar:'baz' } }, -obj2 = { foo:{ baz:'qux' } }, -deepmerge = core.locate('core/deepmerge') - -deepmerge.merge(obj1, obj2) -``` - -In above example, after merge, `obj1` will hold the following value. - -```js -{ - foo: - { - bar:'baz', - baz:'qux' - } -} -``` - -As shown by the example above, the `merge` method extends the object `obj1` with the attributes from `obj2`, without replacing the `foo` reference in `obj1` with the `foo` reference in `obj2`. - ---- - -## Merge arrays - -Deepmerge will extend an array found in one object with the values in another array of the other object, as shown in the example below. - -```js -const -obj1 = { foo:['bar'] }, -obj2 = { foo:['baz'] }, -deepmerge = core.locate('core/deepmerge') - -deepmerge.merge(obj1, obj2) -``` - -In above example, after merge, `obj1` will hold the following value. - -```js -{ - foo: ['bar','baz'] -} -``` - ---- - -## Merge multiple objects - -It is possible to merge multiple objects at the same time, as the example below shows. - - -```js -const -obj1 = { foo:{ foo:'baz' } }, -obj2 = { foo:{ bar:'qux' } }, -obj3 = { bar:{ baz:'qux' } }, -deepmerge = core.locate('core/deepmerge') - -deepmerge.merge(obj1, obj2, obj3) -``` - -In above example, after merge, `obj1` will hold the following value. - -```js -{ - foo: - { - foo:'baz', - bar:'qux' - }, - bar: - { - baz:'qux' - } -} -``` - -In the above example 3 different objects are merged. There is no limit by the component how many objects you can merge at the same time. - ---- - -## Merge conflicts - -If a conflict in the merge is present, then the last object in the merge will be prioritized as the resulting value. - - -```js -const -obj1 = { foo:'bar' }, -obj2 = { foo:'baz' }, -deepmerge = core.locate('core/deepmerge') - -deepmerge.merge(obj1, obj2) -``` - -In above example, after merge, `obj1` will hold the following value. - -```js -{ - foo:'baz' -} -``` - -As the example above shows, the `foo` reference was present in both objects. A string can not be extended by the `core/deepmerge` component, and will therefor replace the value. +# Deepmerge + +This component addresses the problem to merge a nested, and sometimes complicated, object structure, with one ore many others objects. + +```js +const +obj1 = { foo:{ bar:'baz' } }, +obj2 = { foo:{ baz:'qux' } }, +deepmerge = core.locate('core/deepmerge') + +deepmerge.merge(obj1, obj2) +``` + +In above example, after merge, `obj1` will hold the following value. + +```js +{ + foo: + { + bar:'baz', + baz:'qux' + } +} +``` + +As shown by the example above, the `merge` method extends the object `obj1` with the attributes from `obj2`, without replacing the `foo` reference in `obj1` with the `foo` reference in `obj2`. + +--- + +## Merge arrays + +Deepmerge will extend an array found in one object with the values in another array of the other object, as shown in the example below. + +```js +const +obj1 = { foo:['bar'] }, +obj2 = { foo:['baz'] }, +deepmerge = core.locate('core/deepmerge') + +deepmerge.merge(obj1, obj2) +``` + +In above example, after merge, `obj1` will hold the following value. + +```js +{ + foo: ['bar','baz'] +} +``` + +--- + +## Merge multiple objects + +It is possible to merge multiple objects at the same time, as the example below shows. + + +```js +const +obj1 = { foo:{ foo:'baz' } }, +obj2 = { foo:{ bar:'qux' } }, +obj3 = { bar:{ baz:'qux' } }, +deepmerge = core.locate('core/deepmerge') + +deepmerge.merge(obj1, obj2, obj3) +``` + +In above example, after merge, `obj1` will hold the following value. + +```js +{ + foo: + { + foo:'baz', + bar:'qux' + }, + bar: + { + baz:'qux' + } +} +``` + +In the above example 3 different objects are merged. There is no limit by the component how many objects you can merge at the same time. + +--- + +## Merge conflicts + +If a conflict in the merge is present, then the last object in the merge will be prioritized as the resulting value. + + +```js +const +obj1 = { foo:'bar' }, +obj2 = { foo:'baz' }, +deepmerge = core.locate('core/deepmerge') + +deepmerge.merge(obj1, obj2) +``` + +In above example, after merge, `obj1` will hold the following value. + +```js +{ + foo:'baz' +} +``` + +As the example above shows, the `foo` reference was present in both objects. A string can not be extended by the `core/deepmerge` component, and will therefor replace the value. diff --git a/doc/book/1-core-components/eventbus.md b/doc/book/1-core-components/eventbus.md index 4d59601..de180bc 100644 --- a/doc/book/1-core-components/eventbus.md +++ b/doc/book/1-core-components/eventbus.md @@ -1,57 +1,57 @@ -# Eventbus - -The `core/eventbus` component is a bus that extends the `events` module in NodeJs. If no observer is attached to the event that is being emitted, then a warning will be written to the console, through the `core/console` component. - -```js -const eventbus = core.locate('core/eventbus') - -eventbus.on('foo', (event) => /* ... */) -eventbus.emit('foo', 'bar') -``` - -In above example, the `event` argument passed to the observer hold the string value `bar`. - ---- - -## Mapping an observer to listen to an event - -You can define observers in the configuration file of a component you add to the core context, under the namespace `core.eventbus.observers`. Below follows an example of a configuration that attaches an observer to the eventbus. - -```js -{ - core: - { - eventbus: - { - observers: - { - 'foobar': { 'foobar/observer' : true } - } - }, - locator: - { - 'foobar/observer' : '/absolute/path/to/observer' - } - } -} -``` - -As seen by the example above, we define an observer called `foobar/observer` that we map to observe the event `foobar`. We rely on the locator to locate the observer for us. Locating the observer, and attaching the observer to observe the event, will take place during the core bootstrap process. - -The trueful flag in the mapper is indicating that the observer will observe the event. It is sometimes necessery to deactivate some observers when for instance running in a test environment. If you need to deactivate an observer, set the flag to false. - ---- - -## The observer interface - -An observer is a class that follows a simple interface. The class must have an `observe` method that accepts 2 arguements, the emitted data and the eventName of the triggered event. - -```js -class FoobarObserver -{ - observe(data, eventName) - { - /* ... */ - } -} -``` +# Eventbus + +The `core/eventbus` component is a bus that extends the `events` module in NodeJs. If no observer is attached to the event that is being emitted, then a warning will be written to the console, through the `core/console` component. + +```js +const eventbus = core.locate('core/eventbus') + +eventbus.on('foo', (event) => /* ... */) +eventbus.emit('foo', 'bar') +``` + +In above example, the `event` argument passed to the observer hold the string value `bar`. + +--- + +## Mapping an observer to listen to an event + +You can define observers in the configuration file of a component you add to the core context, under the namespace `core.eventbus.observers`. Below follows an example of a configuration that attaches an observer to the eventbus. + +```js +{ + core: + { + eventbus: + { + observers: + { + 'foobar': { 'foobar/observer' : true } + } + }, + locator: + { + 'foobar/observer' : '/absolute/path/to/observer' + } + } +} +``` + +As seen by the example above, we define an observer called `foobar/observer` that we map to observe the event `foobar`. We rely on the locator to locate the observer for us. Locating the observer, and attaching the observer to observe the event, will take place during the core bootstrap process. + +The trueful flag in the mapper is indicating that the observer will observe the event. It is sometimes necessery to deactivate some observers when for instance running in a test environment. If you need to deactivate an observer, set the flag to false. + +--- + +## The observer interface + +An observer is a class that follows a simple interface. The class must have an `observe` method that accepts 2 arguements, the emitted data and the eventName of the triggered event. + +```js +class FoobarObserver +{ + observe(data, eventName) + { + /* ... */ + } +} +``` diff --git a/doc/book/1-core-components/http-request.md b/doc/book/1-core-components/http-request.md index 7dc3860..c7f0e3f 100644 --- a/doc/book/1-core-components/http-request.md +++ b/doc/book/1-core-components/http-request.md @@ -1,52 +1,52 @@ -# Http / Request - -The component `core/http/request` is an extention of the fristanding `@superhero/request` module at npm. Go check out the documentation for that module to find out more specificly the current possible settings and abilities to work with this component. - -This documentation will go through basic use of the component. - ---- - -## Basic use - -The purpose of the component is to make http requests. - -```js -const result = await core.locate('core/http/request').get('http://example.com/') -``` - -In the above example, the value of the result variable contains the following value. - -```js -{ - data : '...', - headers : { /* ... */ }, - status : 200 -} -``` - ---- - -## Configurations - -You can specify options through the namespace `core.http.request.options`. See the `@superhero/request` module at npm for more information about different configurations you can set on construction. Below follows an example of how to set a few configurations through a component config file. - -```js -{ - core: - { - http: - { - request: - { - options: - { - debug : true, - timeout : 60e3 - } - } - } - } -} -``` - -Above example will configure the core contexts `core/http/request` component to print debuggable log messages to the console, and sets the timeout limit to 1 minute. +# Http / Request + +The component `core/http/request` is an extention of the fristanding `@superhero/request` module at npm. Go check out the documentation for that module to find out more specificly the current possible settings and abilities to work with this component. + +This documentation will go through basic use of the component. + +--- + +## Basic use + +The purpose of the component is to make http requests. + +```js +const result = await core.locate('core/http/request').get('http://example.com/') +``` + +In the above example, the value of the result variable contains the following value. + +```js +{ + data : '...', + headers : { /* ... */ }, + status : 200 +} +``` + +--- + +## Configurations + +You can specify options through the namespace `core.http.request.options`. See the `@superhero/request` module at npm for more information about different configurations you can set on construction. Below follows an example of how to set a few configurations through a component config file. + +```js +{ + core: + { + http: + { + request: + { + options: + { + debug : true, + timeout : 60e3 + } + } + } + } +} +``` + +Above example will configure the core contexts `core/http/request` component to print debuggable log messages to the console, and sets the timeout limit to 1 minute. diff --git a/doc/book/1-core-components/http-server.md b/doc/book/1-core-components/http-server.md index 99fcb38..80c01b5 100644 --- a/doc/book/1-core-components/http-server.md +++ b/doc/book/1-core-components/http-server.md @@ -1,304 +1,304 @@ -# Http / Server - -Use the http server component to listen to http network traffic on a specific port, shown below how to specify. - -```js -core.locate('core/http/server').listen(80) -``` - ---- - -## Http / Server / Router - -After a connection is established, we must define a configuration for the router component to know how to handle each request. Below is an example of a route. - -```js -module.exports = -{ - core: - { - http: - { - server: - { - routes: - { - 'foobar': - { - url : '/foobar', - method : 'get', - headers :{'x-foo':'bar'}, - endpoint : 'api/endpoint/foobar', - middleware :['api/middleware/foobar'], - view : 'core/http/server/view/json', - input : 'dto/query-foobar', - output : 'entity/foobar' - } - } - } - } - } -} -``` - -It is possible, and probably expected, that multiple routes are defined in this scope. Which route is to be used depends on a set of validation rules. - -Each variable is exaplained below, what they mean and do in the context of routing. - ---- - -### `url` - -**The `url` variable is a routing variable. A matching url is required when validating to use the defined route.** - -The url is the requested filepath, by example; in the http request `http://example.com/foobar?baz=qux`, the filepath, the `url` variable, is the `/foobar` segment. - -It is possible to use some regular expressions, regex, in the `url` variable to accomplish a dynamic match. But it is considered by this documentaion regarded as good practice to not use any regex, but instead name segments as described below. - -Naming segments in the url can be done in the following way; `/foobar/:baz`, where the `:baz` key is replaced with a regex; `[^/]+`, matching anything but the `/`, the forward slash. Naming a segment is useful for semantikal reasons, but more importantly, named segments are also recognized when composing the dto, **D**ata **T**ransfer **O**bject, as explained under the title `input` below. - -It is possible to assign an expected value for a named segment in the url. Instead of replacing the segment with the regex `[^/]+`, the expected value would be used. Example: `/foobar/:baz=qux` would match with the string `/foobar/qux`. - ---- - -### `method` - -**The `method` variable is a routing variable. A matching url is required when validating to use the defined route.** - -Every http request defines a request method, HTTP verbs, which indicates a desired operation accociated for an endpoint, or a resource. - -Examples of a method commonly used in http api design, in restful design, is `GET`, `PUT`, `POST` and `DELETE`. - -The `method` variable is matched case insensitive. - ---- - -### `headers` - -**The `headers` variable is a routing variable. A matching url is required when validating to use the defined route.** - -In an http request, it is possible to specify headers, the framework allows developers to route based on the value of one or several of the request headers. - -It is possible to match the value of the headers value with regex syntax, shown in below route example: - -```js -{ - url : '/foobar', - method : 'get', - headers : - { - 'x-foo' : 'foo', - 'x-bar' : 'b.+' - }, - endpoint : 'api/endpoint/foobar', - input : 'dto/example', - output : false -} -``` - -The `headers` variables are always matched case insensitive. - ---- - -### `endpoint` - -**When a request is validated to a matching route**, the `endpoint` variable is used as a reference to the dispatcher that will be constructed and used to dispatch the request. - -Below is an example of a dispatcher that simply replies with a `Hello World` string. - -```js -const Dispatcher = require('superhero/core/http/server/dispatcher') - -class Endpoint extends Dispatcher -{ - dispatch() - { - this.view.body = 'Hello World' - } -} - -module.exports = Endpoint -``` - -The `dispatch` method will be awaited, as such; the `dispatch` method be defined with the keyword `async`, if the dispatcher is required to work with asynchronous logic. - ---- - -### `middleware` - -**When a request is validated to a matching route**, the `middleware` variable is used as a reference to a chain of dispatchers that will be constructed and used to perform pre or post dispatcher operations related to the request. - -Below is an example of a middleware dispatcher that simply logs timestamps in between the requests. - -```js -const Dispatcher = require('superhero/core/http/server/dispatcher') - -class Middleware extends Dispatcher -{ - async dispatch(next) - { - console.log(Date.now()) - - await next() - - console.log(Date.now()) - } -} - -module.exports = Middleware -``` - -As seen in the above, we use the same dispatcher for the endpoint dispatcher as we use for the middleware dispatcher. The difference is the passed along `next` function that the implementation must call to trigger the next instance in the dispatcher chain, that finally will call the endpoint, where the chain will collaps on it self as described below. - -``` -Middleware - ↓ ↑ -Middleware - ↓ ↑ - Endpoint -``` - -Worth noting, the post operations will be done in reveresed order to the pre operations due to the collapsed flow. - ---- - -### `view` - -**When a request is validated to a matching route**, the `view` variable is used as a reference to a presenter that will be used to render the output. - -The framework uses by default the view called `core/http/server/view`, which will simply output the `view.body`, defined by the dispatcher process, as a string to the output. - -Additionally view presenters are the following: - -- `core/http/server/view/json` -- `core/http/server/view/stream` -- `core/http/server/view/text` - -The framework also allows for external resources to be used by reference; `@superhero/core.handlebars`, which is publiched on npm, is such an example. - ---- - -### `input` - -**When a request is validated to a matching route**, the `input` variable is used as a reference to a schema that will be used as a template for the dto. - -The dto is a merged, filtered and validated set of data, recognized by the router. Data can be sent as a query variable, sent in the message body, or mapped in the url segments. - -The hiearky of the merged data model is `body` -**→** `query` **→** `url segment`, where the last overwrites the former. - -It is possible to extend the route builder model by adding an additional dto builder to the list of builders in the configuration. - -```js -{ - core: - { - http: - { - server: - { - route: - { - builder: - { - dto: - { - builders: - { - 'key' : 'service' - } - } - } - } - } - } - } -} -``` - -The example above is an abstract defintion of how to add a builder. The key is a unique identifier, designed by a unique index to be able to write over an already existing builder, for what ever reason that would be necessery. The "service" expressed in the value is the service name that will be located by the `core/locator` component. - -A builder is expected to implement the `HttpServerRouteBuilderDtoBuilder` contract. - ---- - -### `output` - -The optional variable `output` is a reference to a schema that is used for automated generation of documentation, but could also be used in other contexts, such as in a middleware to validate that the the output is as expected. - ---- - -## Route termination - -When a request is made, a route walk is begun to find matching routes. The walk will end when a terminating valid route has been found. What defines a terminating route is when a valid route has an endpoint defined. This behavior allows a route building process where multiple valid routes can be merged together as one, as the following example shows. - -```js -module.exports = -{ - core: - { - http: - { - routes: - { - 'json-view': - { - view : 'core/http/server/view/json' - }, - 'log': - { - middleware :['api/middleware/logger'] - }, - 'foo': - { - url : '/foo', - method : 'get', - endpoint : 'api/endpoint/foo', - input : 'dto/query-foo', - output : 'entity/foo' - }, - 'bar': - { - url : '/bar', - method : 'get', - endpoint : 'api/endpoint/bar', - input : 'dto/query-bar', - output : 'entity/bar' - }, - '404': - { - endpoint : 'api/endpoint/not-found', - input : false - } - } - } - } -} -``` - -In above example, both the routes `foo` and `bar` inherites the variable defintions; `view` - from the `json-view` route, and the `middleware` - from the `log` route. - -More explicitly, if a request was made; `http://example.com/bar`, the composed view would look as the following example shows. - -```js -{ - url : '/bar', - method : 'get', - endpoint : 'api/endpoint/bar', - middleware :['api/middleware/logger'], - view : 'core/http/server/view/json', - input : 'dto/query-bar', - output : 'entity/bar' -} -``` - -While the request; `http://example.com/wtf/path`, will have no matching url and compose the following route. - -```js -{ - endpoint : 'api/endpoint/not-found', - middleware :['api/middleware/logger'], - view : 'core/http/server/view/json', - input : false -} -``` +# Http / Server + +Use the http server component to listen to http network traffic on a specific port, shown below how to specify. + +```js +core.locate('core/http/server').listen(80) +``` + +--- + +## Http / Server / Router + +After a connection is established, we must define a configuration for the router component to know how to handle each request. Below is an example of a route. + +```js +module.exports = +{ + core: + { + http: + { + server: + { + routes: + { + 'foobar': + { + url : '/foobar', + method : 'get', + headers :{'x-foo':'bar'}, + endpoint : 'api/endpoint/foobar', + middleware :['api/middleware/foobar'], + view : 'core/http/server/view/json', + input : 'dto/query-foobar', + output : 'entity/foobar' + } + } + } + } + } +} +``` + +It is possible, and probably expected, that multiple routes are defined in this scope. Which route is to be used depends on a set of validation rules. + +Each variable is exaplained below, what they mean and do in the context of routing. + +--- + +### `url` + +**The `url` variable is a routing variable. A matching url is required when validating to use the defined route.** + +The url is the requested filepath, by example; in the http request `http://example.com/foobar?baz=qux`, the filepath, the `url` variable, is the `/foobar` segment. + +It is possible to use some regular expressions, regex, in the `url` variable to accomplish a dynamic match. But it is considered by this documentaion regarded as good practice to not use any regex, but instead name segments as described below. + +Naming segments in the url can be done in the following way; `/foobar/:baz`, where the `:baz` key is replaced with a regex; `[^/]+`, matching anything but the `/`, the forward slash. Naming a segment is useful for semantikal reasons, but more importantly, named segments are also recognized when composing the dto, **D**ata **T**ransfer **O**bject, as explained under the title `input` below. + +It is possible to assign an expected value for a named segment in the url. Instead of replacing the segment with the regex `[^/]+`, the expected value would be used. Example: `/foobar/:baz=qux` would match with the string `/foobar/qux`. + +--- + +### `method` + +**The `method` variable is a routing variable. A matching url is required when validating to use the defined route.** + +Every http request defines a request method, HTTP verbs, which indicates a desired operation accociated for an endpoint, or a resource. + +Examples of a method commonly used in http api design, in restful design, is `GET`, `PUT`, `POST` and `DELETE`. + +The `method` variable is matched case insensitive. + +--- + +### `headers` + +**The `headers` variable is a routing variable. A matching url is required when validating to use the defined route.** + +In an http request, it is possible to specify headers, the framework allows developers to route based on the value of one or several of the request headers. + +It is possible to match the value of the headers value with regex syntax, shown in below route example: + +```js +{ + url : '/foobar', + method : 'get', + headers : + { + 'x-foo' : 'foo', + 'x-bar' : 'b.+' + }, + endpoint : 'api/endpoint/foobar', + input : 'dto/example', + output : false +} +``` + +The `headers` variables are always matched case insensitive. + +--- + +### `endpoint` + +**When a request is validated to a matching route**, the `endpoint` variable is used as a reference to the dispatcher that will be constructed and used to dispatch the request. + +Below is an example of a dispatcher that simply replies with a `Hello World` string. + +```js +const Dispatcher = require('superhero/core/http/server/dispatcher') + +class Endpoint extends Dispatcher +{ + dispatch() + { + this.view.body = 'Hello World' + } +} + +module.exports = Endpoint +``` + +The `dispatch` method will be awaited, as such; the `dispatch` method be defined with the keyword `async`, if the dispatcher is required to work with asynchronous logic. + +--- + +### `middleware` + +**When a request is validated to a matching route**, the `middleware` variable is used as a reference to a chain of dispatchers that will be constructed and used to perform pre or post dispatcher operations related to the request. + +Below is an example of a middleware dispatcher that simply logs timestamps in between the requests. + +```js +const Dispatcher = require('superhero/core/http/server/dispatcher') + +class Middleware extends Dispatcher +{ + async dispatch(next) + { + console.log(Date.now()) + + await next() + + console.log(Date.now()) + } +} + +module.exports = Middleware +``` + +As seen in the above, we use the same dispatcher for the endpoint dispatcher as we use for the middleware dispatcher. The difference is the passed along `next` function that the implementation must call to trigger the next instance in the dispatcher chain, that finally will call the endpoint, where the chain will collaps on it self as described below. + +``` +Middleware + ↓ ↑ +Middleware + ↓ ↑ + Endpoint +``` + +Worth noting, the post operations will be done in reveresed order to the pre operations due to the collapsed flow. + +--- + +### `view` + +**When a request is validated to a matching route**, the `view` variable is used as a reference to a presenter that will be used to render the output. + +The framework uses by default the view called `core/http/server/view`, which will simply output the `view.body`, defined by the dispatcher process, as a string to the output. + +Additionally view presenters are the following: + +- `core/http/server/view/json` +- `core/http/server/view/stream` +- `core/http/server/view/text` + +The framework also allows for external resources to be used by reference; `@superhero/core.handlebars`, which is publiched on npm, is such an example. + +--- + +### `input` + +**When a request is validated to a matching route**, the `input` variable is used as a reference to a schema that will be used as a template for the dto. + +The dto is a merged, filtered and validated set of data, recognized by the router. Data can be sent as a query variable, sent in the message body, or mapped in the url segments. + +The hiearky of the merged data model is `body` +**→** `query` **→** `url segment`, where the last overwrites the former. + +It is possible to extend the route builder model by adding an additional dto builder to the list of builders in the configuration. + +```js +{ + core: + { + http: + { + server: + { + route: + { + builder: + { + dto: + { + builders: + { + 'key' : 'service' + } + } + } + } + } + } + } +} +``` + +The example above is an abstract defintion of how to add a builder. The key is a unique identifier, designed by a unique index to be able to write over an already existing builder, for what ever reason that would be necessery. The "service" expressed in the value is the service name that will be located by the `core/locator` component. + +A builder is expected to implement the `HttpServerRouteBuilderDtoBuilder` contract. + +--- + +### `output` + +The optional variable `output` is a reference to a schema that is used for automated generation of documentation, but could also be used in other contexts, such as in a middleware to validate that the the output is as expected. + +--- + +## Route termination + +When a request is made, a route walk is begun to find matching routes. The walk will end when a terminating valid route has been found. What defines a terminating route is when a valid route has an endpoint defined. This behavior allows a route building process where multiple valid routes can be merged together as one, as the following example shows. + +```js +module.exports = +{ + core: + { + http: + { + routes: + { + 'json-view': + { + view : 'core/http/server/view/json' + }, + 'log': + { + middleware :['api/middleware/logger'] + }, + 'foo': + { + url : '/foo', + method : 'get', + endpoint : 'api/endpoint/foo', + input : 'dto/query-foo', + output : 'entity/foo' + }, + 'bar': + { + url : '/bar', + method : 'get', + endpoint : 'api/endpoint/bar', + input : 'dto/query-bar', + output : 'entity/bar' + }, + '404': + { + endpoint : 'api/endpoint/not-found', + input : false + } + } + } + } +} +``` + +In above example, both the routes `foo` and `bar` inherites the variable defintions; `view` - from the `json-view` route, and the `middleware` - from the `log` route. + +More explicitly, if a request was made; `http://example.com/bar`, the composed view would look as the following example shows. + +```js +{ + url : '/bar', + method : 'get', + endpoint : 'api/endpoint/bar', + middleware :['api/middleware/logger'], + view : 'core/http/server/view/json', + input : 'dto/query-bar', + output : 'entity/bar' +} +``` + +While the request; `http://example.com/wtf/path`, will have no matching url and compose the following route. + +```js +{ + endpoint : 'api/endpoint/not-found', + middleware :['api/middleware/logger'], + view : 'core/http/server/view/json', + input : false +} +``` diff --git a/doc/book/1-core-components/index.md b/doc/book/1-core-components/index.md index ab2db97..3695fb3 100644 --- a/doc/book/1-core-components/index.md +++ b/doc/book/1-core-components/index.md @@ -1,140 +1,140 @@ -# Core - -The framework has a core component. The core component is a contextual scope that contains "instances", or "services", that are isolated to the scope. - -To instanciate a core component you should use the factory, as described bellow. - -```js -const -CoreFactory = require('superhero/core/factory'), -coreFactory = new CoreFactory, -core = coreFactory.create() -``` - -The factory will add all of the core components that can be found in the source code; under the directory "core". Each component offers different functionality and is documented. See specific chapters if you like to learn more about what solutions are offered. - ---- - -## Local component - -Additionaly to the default core components described above, does that are added by default, you are expected to add your own components. The components you add can be domain specific, or extension libraries that your domain is dependent on. - -```js -core.add('foobar') -``` - -Above is a simple example of how to add a component called `foobar`. This will tell the core instance that there is a component called `foobar` in the root of the project. The root is determined by where the main `index.js` file is located. The name `foobar` is actually a relative path that is used as a unique identifier for the component. If you where to add a component that is located in a subfolder, you could instead specify that path. The following example will add a component that is located in the path relative to the main script at `foobar/baz/qux`. - -```js -core.add('foobar/baz/qux') -``` - ---- - -## External component - -You can also add external components in the same way. The core component will first try to load the local reference, but if the local reference fails, then the core will attempt to load the component as an external resource. Loading an "handlebars" component could look something like the following example. - -```js -core.add('@superhero/core.handlebars') -``` - -The prefixed at-symbol; `@` is part of the npm syntax, it is necessery in this example only becouse the module is published under the `superhero` username. If the module you like to use does not have this prefix, then the core will still load the component. The componet in the example is published at npm, and must first be added to your `package.json` file to be refferencable as the example shows. - -```json -{ - "dependencies": - { - "@superhero/core.handlebars" : "*" - } -} -``` - ---- - -## Add a component by absolute path - -It is also possible to add a component by name while specifying an abolute path. - -```js -core.add('foobar', __dirname + '/../foobar') -``` - -The above example will load a component that is sibling to the main path, given that the code example is executed in the main script. It is a useful behaviour, for example if you have a seperate test directory that is a sibling directory to where your source code is located, and you need to use different configurations for testing then in production. - ---- - -## Replace default behaviour - -By default, the core factory adds different modules. One of which is the `core/console` component. The `core/console` component is used when writing to the console. Perhaps you do not like the behaviour of the component, and like to replace the component with a different implementation. You can then simply write your own implementation in the same relevant directory structure as the name of the component. In this case in the directory: `core/console`. Or simply specify the name with the absolute path: - -```js -core.add('core/console', 'some-directory-path/console') -``` - -It is recomended that your follow the interface of the default servivce that you replace, but it is not a requirement. If the behaviour is used by the core however, and not implemented in the replacement component, then errors are expected to be thrown. - ---- - -## Remove a component - -It is also possible to remove an already added components by the `remove` method. - -```js -core.remove('http/server') -``` - -By default, all core components are added to the context, if you like to optimize what components your application is dependent on, then you need to remove them manually, as the example above shows. - -It is only possible to remove an added component before the load process has been triggered. - ---- - -## What is a component - -All components are not valid. A valid component is required to have a configuration file in the directory root of the component. This configuration file `config.js` is important as it defines how the component pluggs into the functionality of the framework. How to plugg into specific parts of the framework should be understood by reading the specific documentations of the specific component you like to plugg into. - -An example of a configuration file follows below. - -```js -{ - core: - { - locator: - { - 'foobar' : '/path/to/locatable/resource' - } - } -} -``` - -Above configuration must be exported through the normal `module.exports` in the file `config.js` located in the root of the component. - ----- - -## Loading the core context - -Once you have added the components that you like to use for your application, you must load the context. Loading the context is done as the example below shows. - -```js -core.load() -``` - -When loading, the core will validate the components added, and merge there configuration files together as one. This means that you can write over configurations set by one component in another. This behaviour can have unexpected consequenzes if you are not careful. The possibility to change configurations that is already defined is allowed by design. The purpose of being able to write over configured variables is for example important when you use the same code base in different contexts - if you are testing, or using a development environment, you will probably not connect to the same infrastructure that is used by your application in production. - -Next, the `load` operation will also eager-load all services defined in the merged configuration file into the service locator, described closer in documentation about the component `core/locator`. The locator component, though it is a simple concept, it is central to the framework and deserves some extra attention. - ---- - -## Bootstrapping - -The bootstrap component `core/bootstrap` is mentioned here as it more or less is always required to bootstrap the application before you start your application specific logic. - -```js -core.locate('core/bootstrap').bootstrap().then(() => -{ - // ... application logic goes here -}) -``` - -You can read more about the bootstrap process, what it does and how you can integrate with the process, under the chapter that documents the component. +# Core + +The framework has a core component. The core component is a contextual scope that contains "instances", or "services", that are isolated to the scope. + +To instanciate a core component you should use the factory, as described bellow. + +```js +const +CoreFactory = require('superhero/core/factory'), +coreFactory = new CoreFactory, +core = coreFactory.create() +``` + +The factory will add all of the core components that can be found in the source code; under the directory "core". Each component offers different functionality and is documented. See specific chapters if you like to learn more about what solutions are offered. + +--- + +## Local component + +Additionaly to the default core components described above, does that are added by default, you are expected to add your own components. The components you add can be domain specific, or extension libraries that your domain is dependent on. + +```js +core.add('foobar') +``` + +Above is a simple example of how to add a component called `foobar`. This will tell the core instance that there is a component called `foobar` in the root of the project. The root is determined by where the main `index.js` file is located. The name `foobar` is actually a relative path that is used as a unique identifier for the component. If you where to add a component that is located in a subfolder, you could instead specify that path. The following example will add a component that is located in the path relative to the main script at `foobar/baz/qux`. + +```js +core.add('foobar/baz/qux') +``` + +--- + +## External component + +You can also add external components in the same way. The core component will first try to load the local reference, but if the local reference fails, then the core will attempt to load the component as an external resource. Loading an "handlebars" component could look something like the following example. + +```js +core.add('@superhero/core.handlebars') +``` + +The prefixed at-symbol; `@` is part of the npm syntax, it is necessery in this example only becouse the module is published under the `superhero` username. If the module you like to use does not have this prefix, then the core will still load the component. The componet in the example is published at npm, and must first be added to your `package.json` file to be refferencable as the example shows. + +```json +{ + "dependencies": + { + "@superhero/core.handlebars" : "*" + } +} +``` + +--- + +## Add a component by absolute path + +It is also possible to add a component by name while specifying an abolute path. + +```js +core.add('foobar', __dirname + '/../foobar') +``` + +The above example will load a component that is sibling to the main path, given that the code example is executed in the main script. It is a useful behaviour, for example if you have a seperate test directory that is a sibling directory to where your source code is located, and you need to use different configurations for testing then in production. + +--- + +## Replace default behaviour + +By default, the core factory adds different modules. One of which is the `core/console` component. The `core/console` component is used when writing to the console. Perhaps you do not like the behaviour of the component, and like to replace the component with a different implementation. You can then simply write your own implementation in the same relevant directory structure as the name of the component. In this case in the directory: `core/console`. Or simply specify the name with the absolute path: + +```js +core.add('core/console', 'some-directory-path/console') +``` + +It is recomended that your follow the interface of the default servivce that you replace, but it is not a requirement. If the behaviour is used by the core however, and not implemented in the replacement component, then errors are expected to be thrown. + +--- + +## Remove a component + +It is also possible to remove an already added components by the `remove` method. + +```js +core.remove('http/server') +``` + +By default, all core components are added to the context, if you like to optimize what components your application is dependent on, then you need to remove them manually, as the example above shows. + +It is only possible to remove an added component before the load process has been triggered. + +--- + +## What is a component + +All components are not valid. A valid component is required to have a configuration file in the directory root of the component. This configuration file `config.js` is important as it defines how the component pluggs into the functionality of the framework. How to plugg into specific parts of the framework should be understood by reading the specific documentations of the specific component you like to plugg into. + +An example of a configuration file follows below. + +```js +{ + core: + { + locator: + { + 'foobar' : '/path/to/locatable/resource' + } + } +} +``` + +Above configuration must be exported through the normal `module.exports` in the file `config.js` located in the root of the component. + +---- + +## Loading the core context + +Once you have added the components that you like to use for your application, you must load the context. Loading the context is done as the example below shows. + +```js +core.load() +``` + +When loading, the core will validate the components added, and merge there configuration files together as one. This means that you can write over configurations set by one component in another. This behaviour can have unexpected consequenzes if you are not careful. The possibility to change configurations that is already defined is allowed by design. The purpose of being able to write over configured variables is for example important when you use the same code base in different contexts - if you are testing, or using a development environment, you will probably not connect to the same infrastructure that is used by your application in production. + +Next, the `load` operation will also eager-load all services defined in the merged configuration file into the service locator, described closer in documentation about the component `core/locator`. The locator component, though it is a simple concept, it is central to the framework and deserves some extra attention. + +--- + +## Bootstrapping + +The bootstrap component `core/bootstrap` is mentioned here as it more or less is always required to bootstrap the application before you start your application specific logic. + +```js +core.locate('core/bootstrap').bootstrap().then(() => +{ + // ... application logic goes here +}) +``` + +You can read more about the bootstrap process, what it does and how you can integrate with the process, under the chapter that documents the component. diff --git a/doc/book/1-core-components/locator.md b/doc/book/1-core-components/locator.md index 34c3028..96d76db 100644 --- a/doc/book/1-core-components/locator.md +++ b/doc/book/1-core-components/locator.md @@ -1,202 +1,202 @@ -# Locator - -The `core/locator` component is a service locator. This component is central to the core framework. This component has deppandes from many different components in the core libarary, and is referenced in many different documentations of different core components. - -The core component is tightly coupled with the locator, the core component is dependent on the locator. The core component has a load method that will eager load specified services from the components configurations. - ---- - -## Example - -I feel it is best suited to explain the locator component by explicitly explain the expected workflow. Below you find a defined file direcory, followed by code snippets of each file and explanations of each of these files and there purpose. - -``` -app -└── src -│ ├── foobar -│ │ ├── config.js -│ │ ├── index.js -│ │ └── locator.js -│ └── index.js -└── package.json -``` - -As we see in the file structure above, we have a simple `app` with a `src` folder that contains the source code. The `src` folder contains a components called `foobar`. In the root of the `foobar` components folder there is a config file that defines how the component integrates with the core context. - -### app/src/foobar/config.js - -```js -module.exports = -{ - core: - { - locator: - { - 'foobar' : __dirname - } - } -} -``` - -Above is a defintion of a component configuration that defines the locatable service `foobar`. The service `foobar` is defined by the following simple class. - -### app/src/foobar/index.js - -```js -class Foobar -{ - constructor(console) - { - this.console = console - } - - foo(bar) - { - this.console.log(bar) - } -} - -module.exports = Foobar -``` - -The code snippet above defines a class called `Foobar` with the defined dependency `console`. The `Foobar` class has a method called `foo` that accepts an argument called `bar`, that later is logged to the console when invoked. - -As we can see above, the class it self is exported by the NodeJs framework. Note that we are not exporting an instance of the class. The instanciation and construction of the class will be done by the locator, as shown below. - -### app/src/foobar/locator.js - -```js -const -Foobar = require('.'), -LocatorConstituent = require('superhero/core/locator/constituent') - -class FoobarLocator extends LocatorConstituent -{ - locate() - { - const - console = this.locator.locate('core/console'), - foobar = new Foobar(console) - - return foobar - } -} - -module.exports = FoobarLocator -``` - -Above you see a locator constituent. The contract is that a `locate` method must be present in the class. The extention to `superhero/core/locator/constituent` provides access to the `core/locator` component through the variable `this.locator`. - -In above example, the instance `foobar` is created and injected with its dependency, the `core/console` component, which is located through the locator. - -When the locator is being eager loaded by the core context, the `locate` method above is called to locate the instance `foobar`, and can be accessed through the locator, in the same way as we above locate the `core/console` component, or located through the core context, as shown below. - -### app/src/index.js - -```js -const -CoreFactory = require('superhero/core/factory'), -coreFactory = new CoreFactory, -core = coreFactory.create() - -core.add('foobar') -core.load() -core.locate('foobar').foo('Hello World') -``` - -Above code is the main script of this example. This is where the core context is created, defined and loaded. - -First we create the core instance, followed by adding the component `foobar` to the core context, followed by loading the core context - which triggers the eager loading process, where the above defined locator constituent is used to create the instance. - -In the end, the `foobar` component is located and used by passing the the string `Hello World` to the method `foo`, resulting in a log message in the console. - -### app/src/package.json - -```js -{ - "name": "App", - "version": "0.0.1", - "description": "An example app", - "repository": "https://github.com/...", - "license": "MIT", - "main": "src/index.js", - "dependencies": { - "superhero": "*" - } -} -``` - -Above is a simple example of the `package.json` file used in this example application. - ---- - -## Dynamic configuration specification - -To ease work with the locator, in the configuration it is possible to make dynamic references. The example below shows a directory with many different services. - -``` -app -└── src - └── domain - ├── service - │ ├── foo - │ │ ├── index.js - │ │ └── locator.js - │ ├── bar - │ │ ├── index.js - │ │ └── locator.js - │ ├── baz - │ │ ├── index.js - │ │ └── locator.js - │ └── qux - │ ├── index.js - │ └── locator.js - └── config.js -``` - -To include all these services, the configuration in the domain is expected to look something like the example below shows. - -```js -module.exports = -{ - core: - { - locator: - { - 'service/foo' : __dirname + '/service/foo', - 'service/bar' : __dirname + '/service/bar', - 'service/baz' : __dirname + '/service/baz', - 'service/qux' : __dirname + '/service/qux' - } - } -} -``` - -By using an **asterix** `*` in the configuration defintion, to specify a dynamic reference, the same configuration could look like the following example shows. - -```js -module.exports = -{ - core: - { - locator: - { - 'service/*' : __dirname + '/service/*' - } - } -} -``` - ---- - -## Good practice - -It is recommended, and by this documentation considered good practice, that you never create a service that is dependent on the `core/locator` component. The locator pattern should be considered as a framework layer, a layer that you should not be integreated with the projects business logic. - -Service locators consists of a larger scope of defined services. If you describe a dependency in your implementations to this larger scope, you are not explicit in your decleration, which is a bad code standard. - -This concept is appearent when you work with unit testing. If the unit is dependent on a greater scope that needs to be loaded before you can test the unit, you have defined a much larger test then necessery for the unit. - -This concept is also appearent when others read your code, or integrates with it. It is much easier to undersand what an implementation does when an explicit dependency injection is honered. - -If we relate to the service locator as a container of the global scope, then creating a dependency to the locator, is the same as creating a dependency to the global scope. It is concidered good practice to isolate your implementations to the local scope the unit operates in to deflect unexpected side effects. +# Locator + +The `core/locator` component is a service locator. This component is central to the core framework. This component has deppandes from many different components in the core libarary, and is referenced in many different documentations of different core components. + +The core component is tightly coupled with the locator, the core component is dependent on the locator. The core component has a load method that will eager load specified services from the components configurations. + +--- + +## Example + +I feel it is best suited to explain the locator component by explicitly explain the expected workflow. Below you find a defined file direcory, followed by code snippets of each file and explanations of each of these files and there purpose. + +``` +app +└── src +│ ├── foobar +│ │ ├── config.js +│ │ ├── index.js +│ │ └── locator.js +│ └── index.js +└── package.json +``` + +As we see in the file structure above, we have a simple `app` with a `src` folder that contains the source code. The `src` folder contains a components called `foobar`. In the root of the `foobar` components folder there is a config file that defines how the component integrates with the core context. + +### app/src/foobar/config.js + +```js +module.exports = +{ + core: + { + locator: + { + 'foobar' : __dirname + } + } +} +``` + +Above is a defintion of a component configuration that defines the locatable service `foobar`. The service `foobar` is defined by the following simple class. + +### app/src/foobar/index.js + +```js +class Foobar +{ + constructor(console) + { + this.console = console + } + + foo(bar) + { + this.console.log(bar) + } +} + +module.exports = Foobar +``` + +The code snippet above defines a class called `Foobar` with the defined dependency `console`. The `Foobar` class has a method called `foo` that accepts an argument called `bar`, that later is logged to the console when invoked. + +As we can see above, the class it self is exported by the NodeJs framework. Note that we are not exporting an instance of the class. The instanciation and construction of the class will be done by the locator, as shown below. + +### app/src/foobar/locator.js + +```js +const +Foobar = require('.'), +LocatorConstituent = require('superhero/core/locator/constituent') + +class FoobarLocator extends LocatorConstituent +{ + locate() + { + const + console = this.locator.locate('core/console'), + foobar = new Foobar(console) + + return foobar + } +} + +module.exports = FoobarLocator +``` + +Above you see a locator constituent. The contract is that a `locate` method must be present in the class. The extention to `superhero/core/locator/constituent` provides access to the `core/locator` component through the variable `this.locator`. + +In above example, the instance `foobar` is created and injected with its dependency, the `core/console` component, which is located through the locator. + +When the locator is being eager loaded by the core context, the `locate` method above is called to locate the instance `foobar`, and can be accessed through the locator, in the same way as we above locate the `core/console` component, or located through the core context, as shown below. + +### app/src/index.js + +```js +const +CoreFactory = require('superhero/core/factory'), +coreFactory = new CoreFactory, +core = coreFactory.create() + +core.add('foobar') +core.load() +core.locate('foobar').foo('Hello World') +``` + +Above code is the main script of this example. This is where the core context is created, defined and loaded. + +First we create the core instance, followed by adding the component `foobar` to the core context, followed by loading the core context - which triggers the eager loading process, where the above defined locator constituent is used to create the instance. + +In the end, the `foobar` component is located and used by passing the the string `Hello World` to the method `foo`, resulting in a log message in the console. + +### app/src/package.json + +```js +{ + "name": "App", + "version": "0.0.1", + "description": "An example app", + "repository": "https://github.com/...", + "license": "MIT", + "main": "src/index.js", + "dependencies": { + "superhero": "*" + } +} +``` + +Above is a simple example of the `package.json` file used in this example application. + +--- + +## Dynamic configuration specification + +To ease work with the locator, in the configuration it is possible to make dynamic references. The example below shows a directory with many different services. + +``` +app +└── src + └── domain + ├── service + │ ├── foo + │ │ ├── index.js + │ │ └── locator.js + │ ├── bar + │ │ ├── index.js + │ │ └── locator.js + │ ├── baz + │ │ ├── index.js + │ │ └── locator.js + │ └── qux + │ ├── index.js + │ └── locator.js + └── config.js +``` + +To include all these services, the configuration in the domain is expected to look something like the example below shows. + +```js +module.exports = +{ + core: + { + locator: + { + 'service/foo' : __dirname + '/service/foo', + 'service/bar' : __dirname + '/service/bar', + 'service/baz' : __dirname + '/service/baz', + 'service/qux' : __dirname + '/service/qux' + } + } +} +``` + +By using an **asterix** `*` in the configuration defintion, to specify a dynamic reference, the same configuration could look like the following example shows. + +```js +module.exports = +{ + core: + { + locator: + { + 'service/*' : __dirname + '/service/*' + } + } +} +``` + +--- + +## Good practice + +It is recommended, and by this documentation considered good practice, that you never create a service that is dependent on the `core/locator` component. The locator pattern should be considered as a framework layer, a layer that you should not be integreated with the projects business logic. + +Service locators consists of a larger scope of defined services. If you describe a dependency in your implementations to this larger scope, you are not explicit in your decleration, which is a bad code standard. + +This concept is appearent when you work with unit testing. If the unit is dependent on a greater scope that needs to be loaded before you can test the unit, you have defined a much larger test then necessery for the unit. + +This concept is also appearent when others read your code, or integrates with it. It is much easier to undersand what an implementation does when an explicit dependency injection is honered. + +If we relate to the service locator as a container of the global scope, then creating a dependency to the locator, is the same as creating a dependency to the global scope. It is concidered good practice to isolate your implementations to the local scope the unit operates in to deflect unexpected side effects. diff --git a/doc/book/1-core-components/object.md b/doc/book/1-core-components/object.md index 30b1a9c..1788ac1 100644 --- a/doc/book/1-core-components/object.md +++ b/doc/book/1-core-components/object.md @@ -1,54 +1,54 @@ -# Object - -The `core/object` component is an aggregated instance that exposes functionality that operates in relation to the js `Object` type. - ---- - -### `composeLowerCaseKeyedObject` - -The method `composeLowerCaseKeyedObject` will compose a cloned object where all the keys of the input object are returned with lower cased keys. - -```js -const -obj = { FooBar:'FooBar' }, -object = core.locate('core/object'), -composed = object.composeLowerCaseKeyedObject(obj) -``` - -In the above example, value of the `composed` variable is as described below. - -```js -{ foobar:'FooBar' } -``` - ---- - -### `composeObjectWithoutKeys` - -The method `composeObjectWithoutKeys` is authored by Lleonard Subirana. The method will creates a copy of an object, excluding keys defined by the following arguments. - -```js -const -obj = { foo:123, bar:456, baz:789, qux:0 }, -object = core.locate('core/object'), -composed = object.composeObjectWithoutKeys(obj, 'bar', 'baz') -``` - -In the above example, value of the `composed` variable is as described below. - -```js -{ foo:123, qux:0 } -``` - -References are by design kept intact in the composed object, as the following example shows. - -```js -const -obj = { foo:{ bar:'baz' }, baz:456 }, -object = core.locate('core/object'), -composed = object.composeObjectWithoutKeys(obj, 'baz') - -obj.foo.bar = 'qux' -obj.foo.bar === composed.foo.bar -// > true -``` +# Object + +The `core/object` component is an aggregated instance that exposes functionality that operates in relation to the js `Object` type. + +--- + +### `composeLowerCaseKeyedObject` + +The method `composeLowerCaseKeyedObject` will compose a cloned object where all the keys of the input object are returned with lower cased keys. + +```js +const +obj = { FooBar:'FooBar' }, +object = core.locate('core/object'), +composed = object.composeLowerCaseKeyedObject(obj) +``` + +In the above example, value of the `composed` variable is as described below. + +```js +{ foobar:'FooBar' } +``` + +--- + +### `composeObjectWithoutKeys` + +The method `composeObjectWithoutKeys` is authored by Lleonard Subirana. The method will creates a copy of an object, excluding keys defined by the following arguments. + +```js +const +obj = { foo:123, bar:456, baz:789, qux:0 }, +object = core.locate('core/object'), +composed = object.composeObjectWithoutKeys(obj, 'bar', 'baz') +``` + +In the above example, value of the `composed` variable is as described below. + +```js +{ foo:123, qux:0 } +``` + +References are by design kept intact in the composed object, as the following example shows. + +```js +const +obj = { foo:{ bar:'baz' }, baz:456 }, +object = core.locate('core/object'), +composed = object.composeObjectWithoutKeys(obj, 'baz') + +obj.foo.bar = 'qux' +obj.foo.bar === composed.foo.bar +// > true +``` diff --git a/doc/book/1-core-components/path.md b/doc/book/1-core-components/path.md index a32ea84..8fcf26b 100644 --- a/doc/book/1-core-components/path.md +++ b/doc/book/1-core-components/path.md @@ -1,44 +1,44 @@ -# Path - -The `core/path` component is an aggregated instance that exposes functionality in relation to a string representing path to a file or directory. - ---- - -## The path to the main script - -On construction of this instance, the path to the main script filename, and the directory name of that file, are stored and can be accessed through the component in the following way. - -```js -const -path = core.locate('core/path'), -filename = path.main.filename, -dirname = path.main.dirname -``` - ---- - -## Wrapper for NodeJs path module - -The path module in nodejs has a few implementations that are wrapped in this implementation, listed below. - -- `dirname` -- `normalize` -- `extension` -- `isAbsolute` - -See nodejs documentation for each case for further information. - ---- - -### `isResolvable` - -The NodeJs framework has a method called `require.resolve` that will throw an error if the argument is unresolvable. The method `isResolvable` in this implementation has wrppaed that functionality and returns a boolean value in relation to if the filename path is resolvable or not. - -```js -const -filename = 'path/to/a/requireable/nodejs/module', -path = core.locate('core/path'), -resolvable = path.isResolvable(filename) -``` - -In above example, the variable `resolvable` is of boolean type, true if the filename can be required. +# Path + +The `core/path` component is an aggregated instance that exposes functionality in relation to a string representing path to a file or directory. + +--- + +## The path to the main script + +On construction of this instance, the path to the main script filename, and the directory name of that file, are stored and can be accessed through the component in the following way. + +```js +const +path = core.locate('core/path'), +filename = path.main.filename, +dirname = path.main.dirname +``` + +--- + +## Wrapper for NodeJs path module + +The path module in nodejs has a few implementations that are wrapped in this implementation, listed below. + +- `dirname` +- `normalize` +- `extension` +- `isAbsolute` + +See nodejs documentation for each case for further information. + +--- + +### `isResolvable` + +The NodeJs framework has a method called `require.resolve` that will throw an error if the argument is unresolvable. The method `isResolvable` in this implementation has wrppaed that functionality and returns a boolean value in relation to if the filename path is resolvable or not. + +```js +const +filename = 'path/to/a/requireable/nodejs/module', +path = core.locate('core/path'), +resolvable = path.isResolvable(filename) +``` + +In above example, the variable `resolvable` is of boolean type, true if the filename can be required. diff --git a/doc/book/1-core-components/process.md b/doc/book/1-core-components/process.md index 2ba4a47..af8547a 100644 --- a/doc/book/1-core-components/process.md +++ b/doc/book/1-core-components/process.md @@ -1,20 +1,20 @@ -# Process - -The `core/process` component is an aggregated instance that exposes functionality in relation to the process that the script is running in. - ---- - -### `exit` - -A wrapper for the NodeJs `process.exit` method. Simply exit the process by running the method `exit`. - -```js -core.locate('core/process').exit() -``` - ---- - -## Bootstrap - -The component will attach a listener to the process `unhandledRejection` and -`uncaughtException` events. The implementation will throw the errors to the scoped `domain` - if present, else to the `core/eventbus` component as the event `core.error`. +# Process + +The `core/process` component is an aggregated instance that exposes functionality in relation to the process that the script is running in. + +--- + +### `exit` + +A wrapper for the NodeJs `process.exit` method. Simply exit the process by running the method `exit`. + +```js +core.locate('core/process').exit() +``` + +--- + +## Bootstrap + +The component will attach a listener to the process `unhandledRejection` and +`uncaughtException` events. The implementation will throw the errors to the scoped `domain` - if present, else to the `core/eventbus` component as the event `core.error`. diff --git a/doc/book/1-core-components/schema.md b/doc/book/1-core-components/schema.md index 6c45ee0..fd8ef24 100644 --- a/doc/book/1-core-components/schema.md +++ b/doc/book/1-core-components/schema.md @@ -1,578 +1,578 @@ -# Schema - -This component uses a composer to filter and validate a data structure in accordence to a defiend schema. - ---- - -## Availible validators and filters - -In below table you can see which type of validators and filters that are availible to each schema type that is titled in the column. - -| | csv | boolean | decimal | integer | json | schema | string | timestamp | -|----------------------|-----------|-----------|-----------|-----------|-----------|-----------|-----------|-----------| -| default | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | -| collection | boolean | boolean | boolean | boolean | boolean | boolean | boolean | boolean | -| collection-size-min | number | number | number | number | number | number | number | number | -| collection-size-max | number | number | number | number | number | number | number | number | -| optional | boolean | boolean | boolean | boolean | boolean | boolean | boolean | boolean | -| nullable | boolean | boolean | boolean | boolean | boolean | boolean | boolean | boolean | -| unsigned | ✗ | ✗ | boolean | boolean | ✗ | ✗ | ✗ | ✗ | -| min | number | ✗ | number | number | ✗ | ✗ | number | timestamp | -| max | number | ✗ | number | number | ✗ | ✗ | number | timestamp | -| gt | number | ✗ | number | number | ✗ | ✗ | number | timestamp | -| lt | number | ✗ | number | number | ✗ | ✗ | number | timestamp | -| length | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | number | ✗ | -| enum | array | array | array | array | ✗ | ✗ | array | array | -| uppercase | boolean | ✗ | ✗ | ✗ | ✗ | ✗ | boolean | ✗ | -| lowercase | boolean | ✗ | ✗ | ✗ | ✗ | ✗ | boolean | ✗ | -| not-empty | boolean | ✗ | ✗ | ✗ | ✗ | ✗ | boolean | ✗ | -| stringified | ✗ | ✗ | ✗ | ✗ | boolean | ✗ | ✗ | ✗ | -| indentation | ✗ | ✗ | ✗ | ✗ | number | ✗ | ✗ | ✗ | -| schema | ✗ | ✗ | ✗ | ✗ | ✗ | string | ✗ | ✗ | -| trait | ✗ | ✗ | ✗ | ✗ | ✗ | string | ✗ | ✗ | - ---- - -## Example application - -This example will show a simple application with an expected flow that uses schemas at route level, as well as in context-mapper, the infrastructural anti-corruption layer. - -``` -app -├── src -│ ├── api -│ │ ├── endpoint -│ │ │ └── fetch-person.js -│ │ └── config.js -│ ├── domain -│ │ ├── aggregate -│ │ │ └── person -│ │ │ ├── index.js -│ │ │ └── locator.js -│ │ ├── schema -│ │ │ ├── dto -│ │ │ │ └── query-person.js -│ │ │ └── entity -│ │ │ └── person.js -│ │ └── config.js -│ ├── infrastructure -│ │ ├── db -│ │ │ └── repository -│ │ │ ├── index.js -│ │ │ ├── locator.js -│ │ │ └── mapper.js -│ │ └── config.js -│ └── index.js -└── package.json -``` - -Above is an overview of a defined filestructure of an application that has only 1 endpoint, that is to fetch a person. - ---- - -### Api - -The api layer is where the interactable interface to the application is defined. - ---- - -### app/src/api/endpoint/fetch-person.js - -```js -const Dispatcher = require('superhero/core/http/server/dispatcher') - -class EndpointPerson extends Dispatcher -{ - async dispatch() - { - const - aggregate = this.locator.locate('aggregate/person'), - person = await person.fetch(this.route.dto) - - this.view.body = person - } -} - -module.exports = EndpointPerson -``` - -The endpoint above locates the aggregate for the person through wich a person is querried with the help of the dto, **D**ata **T**ransfer **O**bject, mapped by the route. The entity "person" is set to the view to be returned in the api call. - ---- - -### app/src/api/config.js - -```js -module.exports = -{ - core: - { - http: - { - routes: - { - 'fetch-person': - { - url : '/', - method : 'get', - endpoint : 'api/endpoint/fetch-person', - view : 'core/http/server/view/json', - input : 'dto/query-person', - output : 'entity/person' - } - } - } - } -} -``` - -In the configuration of the api layer, we define the route with the `input` and `output` schemas refferenced. - ---- - -### Domain - -The domain layer is where logic related to the context of the application is located. - ---- - -### app/src/domain/aggregate/person/index.js - -```js -class AggregatePerson -{ - constructor(repository) - { - this.repository = repository - } - - fetch(dto) - { - return this.repository.fetchPerson(dto) - } -} - -module.exports = AggregatePerson -``` - -An aggregate is defined in the domain layer that is dependent on a repository that can fetch a person. In DDD, **D**omain **D**riven **D**esign, this dependency is expected to be contract oriented. - ---- - -### app/src/domain/aggregate/person/locator.js - -```js -const -AggregatePerson = require('.'), -LocatorConstituent = require('superhero/core/locator/constituent') - -class AggregatePersonLocator extends LocatorConstituent -{ - locate() - { - const - repository = this.locator.locate('db/repository'), - aggregate = new AggregatePerson(repository) - - return aggregate - } -} - -module.exports = AggregatePersonLocator -``` - -In the locator constituent for the "person" aggregate, the db repository is located and injected into the aggregate on construction. - ---- - -### Domain / Schema - -In a sub layer to the domain we expect to find the schemas that defines the domains data structures. - -The schema layer has different sub layers as well; `dto`, `entity` and `value-object` are some common layers used by the DDD community in this scope. - ---- - -### app/src/domain/schema/dto/query-person.js - -```js -module.exports = -{ - 'id': - { - 'type' : 'integer', - 'unsigned' : true - } -} -``` - -Above is a simple dto schema that defines a query used to fetch a person. As the query only has an attribute named `id` defined, a person can only be fetched by `id` in this example. - ---- - -### app/src/domain/schema/entity/person.js - -```js -module.exports = -{ - 'id': - { - 'type' : 'integer', - 'unsigned' : true - }, - 'name': - { - 'type' : 'string', - 'not-empty' : true - }, - 'age': - { - 'type' : 'integer', - 'unsigned' : true - } -} -``` - -In the entity layer we find the schema for the person defined. The entity has 3 attributes declared; `id`, `name` and `age`. The schema defines validation and filtration rules that are related to the data structure. - ---- - -### app/src/domain/config.js - -```js -module.exports = -{ - core: - { - locator: - { - 'aggregate/person' : __dirname + '/aggregate/person' - }, - schema: - { - composer: - { - 'dto/query-person' : __dirname + '/schema/dto/query-person', - 'entity/person' : __dirname + '/schema/entity/person' - } - } - } -} -``` - -In the configuration file of the domain layer references are decalared to the agregate and the the schemas as shown above. - ---- - -### Infrastructure - -The infrastructure layer is the layer that contains logic related to interaction with external services, such as the database. - ---- - -### app/src/infrastructure/db/gateway/locator.js - -```js -const -mysql = require('mysql'), -Db = require('@superhero/db'), -AdapterFactory = require('@superhero/db/adapter/mysql/factory'), -LocatorConstituent = require('superhero/core/locator/constituent') - -class DbGatewayLocator extends LocatorConstituent -{ - locate() - { - const - configuration = this.locator.locate('core/configuration'), - adapterFactory = new AdapterFactory(), - options = configuration.find('infrastructure/db/gateway'), - filePath = __dirname + '/../sql', - fileSuffix = '.sql', - adapter = adapterFactory.create(mysql, options), - gateway = new Db(adapter, filePath, fileSuffix), - - return gateway - } -} - -module.exports = DbGatewayLocator -``` - -In this example we use the external component `@superhero/db` that is published on npm as the gateway. See in-dept documentation at the public repository for more information. - ---- - -### app/src/infrastructure/db/repository/index.js - -```js -const -DbRepository = require('.'), -LocatorConstituent = require('superhero/core/locator/constituent') - -class DbRepository -{ - constructor(gateway, mapper) - { - this.gateway = gateway - this.mapper = mapper - } - - async fetchPerson(dto) - { - const - result = await this.gateway.query('person/fetch', [dto.id]), - person = this.mapper.mapPerson(result) - - return person - } -} - -module.exports = DbRepository -``` - -As seen in the example above, we have a dependency to the db gateway and a mapper, the context-mapper. The mapper will take the result from the database and map the result to an understandable data structure in accordence to the schema defined in the domain. - ---- - -### app/src/infrastructure/db/repository/locator.js - -```js -const -DbRepository = require('.'), -DbRepositoryMapper = require('./mapper'), -LocatorConstituent = require('superhero/core/locator/constituent') - -class DbRepositoryLocator extends LocatorConstituent -{ - locate() - { - const - gateway = this.locator.locate('db/gateway'), - composer = this.locator.locate('core/schema/composer'), - mapper = new DbRepositoryMapper(composer), - repository = new DbRepository(gateway, mapper) - - return repository - } -} - -module.exports = DbRepositoryLocator -``` - -In the locator for the repository we construct the repository, but we also construct the mapper. The mapper is injected with the located component `core/schema/composer`. - ---- - -### app/src/infrastructure/db/repository/mapper.js - -```js -class DbRepositoryMapper -{ - constructor(composer) - { - this.composer = composer - } - - mapPerson(result) - { - if(result.length < 1) - { - throw new Error('No results found') - } - - if(result.length > 1) - { - throw new Error('Conflicting results found') - } - - return this.composer.compose('entity/person', result[0]) - } -} - -module.exports = DbRepositoryMapper -``` - -In the context-mapper we use the schema composer to validate and filter the result before we return the expected data structure. It is considered good practice to use a composition pattern for the mapper. In this example the mapper is very simple, the mapper only has one responsibility as it is, why I decided to not use a composition pattern in this example. - ---- - -### app/src/infrastructure/db/config.js - -```js -module.exports = -{ - core: - { - locator: - { - 'db/gateway' : __dirname + '/db/gateway', - 'db/repository' : __dirname + '/db/repository' - } - }, - infrastructure: - { - db: - { - gateway: - { - connections : 5, - host : '...', - user : '...', - password : '...', - } - } - } -} -``` - -In the configuration file of the infrastructure we declare the refferences to the location of the services `db/gateway` and `db/repository`. We also define the configuration options to the gateway. The values for `host`, `user` and `password` as 3 dots is placeholders for the values needed in the connection at hand. - ---- - -### app/src/index.js - -```js -const -CoreFactory = require('superhero/core/factory'), -coreFactory = new CoreFactory, -core = coreFactory.create() - -core.add('api') -core.add('domain') -core.add('infrastructure') - -core.load() - -core.locate('core/bootstrap').bootstrap().then(() => -core.locate('core/http/server').listen(80)) -``` - -Finally we define the main script that adds and loads the `api`, `domain` and the `infrastructure` components. After bootstrap, the server is instructed to listen to port `80` for incoming api calls. - ---- - -### app/src/package.json - -```js -{ - "name": "App", - "version": "0.0.1", - "description": "An example app", - "repository": "https://github.com/...", - "license": "MIT", - "main": "src/index.js", - "dependencies": { - "superhero": "*", - "@superhero/db": "*", - "mysql": "*" - } -} -``` - -Below is a simple example of the `package.json` file used in this example application. - ---- - -## Meta definition - -It is possible to define a meta attribute to your schema, which in turn allows 2 different possible meta descriptions; `extends` and `immutable`. - ---- - -### Extends - -From above example, we defined a person in a schema. If we want to have an extended type of a person, a `superhero` which has all the attributes of a person and an additional attribute; `superpower`, then we can use the meta attribute to extend the person as the example below describes. - -```js -module.exports = -{ - '@meta': - { - 'extends' : 'entity/person' - }, - 'superpower': - { - 'type' : 'string', - 'enum' : ['invisibility', 'precognition', 'indestructible'] - } -} -``` - -It is also possible to extend from multiple references by using an array of refferences in the `extends` attribute. - ---- - -### Immutable - -By default, the schema will construct an immutable object, an object that can not be changed, a frozen object. If you like a composed structure to instead be mutable, then you must specify that in the meta attribute, as in the example below. - -```js -module.exports = -{ - '@meta': - { - 'immutable' : false - }, - // ... -} -``` - ---- - -## Dynamic configuration specification - -To make it easier to load schemas, it is possible to use a dynamic reference in the configurations. The example below shows a directory with many different shemas. - -``` -app -└── src - └── domain - ├── schema - │ ├── foo.js - │ ├── bar.js - │ ├── baz.js - │ └── qux.js - └── config.js -``` - -To include all these services, the configuration in the domain is expected to look something like the example below shows. - -```js -module.exports = -{ - core: - { - schema: - { - composer: - { - 'schema/foo' : __dirname + '/schema/foo', - 'schema/bar' : __dirname + '/schema/bar', - 'schema/baz' : __dirname + '/schema/baz', - 'schema/qux' : __dirname + '/schema/qux' - } - } - } -} -``` - -By using an **asterix** `*` in the configuration defintion, to specify a dynamic reference, the same configuration could look like the following example shows. - -```js -module.exports = -{ - core: - { - schema: - { - composer: - { - 'schema/*' : __dirname + '/schema/*' - } - } - } -} -``` - -Both examples above has the same effect. +# Schema + +This component uses a composer to filter and validate a data structure in accordence to a defiend schema. + +--- + +## Availible validators and filters + +In below table you can see which type of validators and filters that are availible to each schema type that is titled in the column. + +| | csv | boolean | decimal | integer | json | schema | string | timestamp | +|----------------------|-----------|-----------|-----------|-----------|-----------|-----------|-----------|-----------| +| default | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | +| collection | boolean | boolean | boolean | boolean | boolean | boolean | boolean | boolean | +| collection-size-min | number | number | number | number | number | number | number | number | +| collection-size-max | number | number | number | number | number | number | number | number | +| optional | boolean | boolean | boolean | boolean | boolean | boolean | boolean | boolean | +| nullable | boolean | boolean | boolean | boolean | boolean | boolean | boolean | boolean | +| unsigned | ✗ | ✗ | boolean | boolean | ✗ | ✗ | ✗ | ✗ | +| min | number | ✗ | number | number | ✗ | ✗ | number | timestamp | +| max | number | ✗ | number | number | ✗ | ✗ | number | timestamp | +| gt | number | ✗ | number | number | ✗ | ✗ | number | timestamp | +| lt | number | ✗ | number | number | ✗ | ✗ | number | timestamp | +| length | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | number | ✗ | +| enum | array | array | array | array | ✗ | ✗ | array | array | +| uppercase | boolean | ✗ | ✗ | ✗ | ✗ | ✗ | boolean | ✗ | +| lowercase | boolean | ✗ | ✗ | ✗ | ✗ | ✗ | boolean | ✗ | +| not-empty | boolean | ✗ | ✗ | ✗ | ✗ | ✗ | boolean | ✗ | +| stringified | ✗ | ✗ | ✗ | ✗ | boolean | ✗ | ✗ | ✗ | +| indentation | ✗ | ✗ | ✗ | ✗ | number | ✗ | ✗ | ✗ | +| schema | ✗ | ✗ | ✗ | ✗ | ✗ | string | ✗ | ✗ | +| trait | ✗ | ✗ | ✗ | ✗ | ✗ | string | ✗ | ✗ | + +--- + +## Example application + +This example will show a simple application with an expected flow that uses schemas at route level, as well as in context-mapper, the infrastructural anti-corruption layer. + +``` +app +├── src +│ ├── api +│ │ ├── endpoint +│ │ │ └── fetch-person.js +│ │ └── config.js +│ ├── domain +│ │ ├── aggregate +│ │ │ └── person +│ │ │ ├── index.js +│ │ │ └── locator.js +│ │ ├── schema +│ │ │ ├── dto +│ │ │ │ └── query-person.js +│ │ │ └── entity +│ │ │ └── person.js +│ │ └── config.js +│ ├── infrastructure +│ │ ├── db +│ │ │ └── repository +│ │ │ ├── index.js +│ │ │ ├── locator.js +│ │ │ └── mapper.js +│ │ └── config.js +│ └── index.js +└── package.json +``` + +Above is an overview of a defined filestructure of an application that has only 1 endpoint, that is to fetch a person. + +--- + +### Api + +The api layer is where the interactable interface to the application is defined. + +--- + +### app/src/api/endpoint/fetch-person.js + +```js +const Dispatcher = require('superhero/core/http/server/dispatcher') + +class EndpointPerson extends Dispatcher +{ + async dispatch() + { + const + aggregate = this.locator.locate('aggregate/person'), + person = await person.fetch(this.route.dto) + + this.view.body = person + } +} + +module.exports = EndpointPerson +``` + +The endpoint above locates the aggregate for the person through wich a person is querried with the help of the dto, **D**ata **T**ransfer **O**bject, mapped by the route. The entity "person" is set to the view to be returned in the api call. + +--- + +### app/src/api/config.js + +```js +module.exports = +{ + core: + { + http: + { + routes: + { + 'fetch-person': + { + url : '/', + method : 'get', + endpoint : 'api/endpoint/fetch-person', + view : 'core/http/server/view/json', + input : 'dto/query-person', + output : 'entity/person' + } + } + } + } +} +``` + +In the configuration of the api layer, we define the route with the `input` and `output` schemas refferenced. + +--- + +### Domain + +The domain layer is where logic related to the context of the application is located. + +--- + +### app/src/domain/aggregate/person/index.js + +```js +class AggregatePerson +{ + constructor(repository) + { + this.repository = repository + } + + fetch(dto) + { + return this.repository.fetchPerson(dto) + } +} + +module.exports = AggregatePerson +``` + +An aggregate is defined in the domain layer that is dependent on a repository that can fetch a person. In DDD, **D**omain **D**riven **D**esign, this dependency is expected to be contract oriented. + +--- + +### app/src/domain/aggregate/person/locator.js + +```js +const +AggregatePerson = require('.'), +LocatorConstituent = require('superhero/core/locator/constituent') + +class AggregatePersonLocator extends LocatorConstituent +{ + locate() + { + const + repository = this.locator.locate('db/repository'), + aggregate = new AggregatePerson(repository) + + return aggregate + } +} + +module.exports = AggregatePersonLocator +``` + +In the locator constituent for the "person" aggregate, the db repository is located and injected into the aggregate on construction. + +--- + +### Domain / Schema + +In a sub layer to the domain we expect to find the schemas that defines the domains data structures. + +The schema layer has different sub layers as well; `dto`, `entity` and `value-object` are some common layers used by the DDD community in this scope. + +--- + +### app/src/domain/schema/dto/query-person.js + +```js +module.exports = +{ + 'id': + { + 'type' : 'integer', + 'unsigned' : true + } +} +``` + +Above is a simple dto schema that defines a query used to fetch a person. As the query only has an attribute named `id` defined, a person can only be fetched by `id` in this example. + +--- + +### app/src/domain/schema/entity/person.js + +```js +module.exports = +{ + 'id': + { + 'type' : 'integer', + 'unsigned' : true + }, + 'name': + { + 'type' : 'string', + 'not-empty' : true + }, + 'age': + { + 'type' : 'integer', + 'unsigned' : true + } +} +``` + +In the entity layer we find the schema for the person defined. The entity has 3 attributes declared; `id`, `name` and `age`. The schema defines validation and filtration rules that are related to the data structure. + +--- + +### app/src/domain/config.js + +```js +module.exports = +{ + core: + { + locator: + { + 'aggregate/person' : __dirname + '/aggregate/person' + }, + schema: + { + composer: + { + 'dto/query-person' : __dirname + '/schema/dto/query-person', + 'entity/person' : __dirname + '/schema/entity/person' + } + } + } +} +``` + +In the configuration file of the domain layer references are decalared to the agregate and the the schemas as shown above. + +--- + +### Infrastructure + +The infrastructure layer is the layer that contains logic related to interaction with external services, such as the database. + +--- + +### app/src/infrastructure/db/gateway/locator.js + +```js +const +mysql = require('mysql'), +Db = require('@superhero/db'), +AdapterFactory = require('@superhero/db/adapter/mysql/factory'), +LocatorConstituent = require('superhero/core/locator/constituent') + +class DbGatewayLocator extends LocatorConstituent +{ + locate() + { + const + configuration = this.locator.locate('core/configuration'), + adapterFactory = new AdapterFactory(), + options = configuration.find('infrastructure/db/gateway'), + filePath = __dirname + '/../sql', + fileSuffix = '.sql', + adapter = adapterFactory.create(mysql, options), + gateway = new Db(adapter, filePath, fileSuffix), + + return gateway + } +} + +module.exports = DbGatewayLocator +``` + +In this example we use the external component `@superhero/db` that is published on npm as the gateway. See in-dept documentation at the public repository for more information. + +--- + +### app/src/infrastructure/db/repository/index.js + +```js +const +DbRepository = require('.'), +LocatorConstituent = require('superhero/core/locator/constituent') + +class DbRepository +{ + constructor(gateway, mapper) + { + this.gateway = gateway + this.mapper = mapper + } + + async fetchPerson(dto) + { + const + result = await this.gateway.query('person/fetch', [dto.id]), + person = this.mapper.mapPerson(result) + + return person + } +} + +module.exports = DbRepository +``` + +As seen in the example above, we have a dependency to the db gateway and a mapper, the context-mapper. The mapper will take the result from the database and map the result to an understandable data structure in accordence to the schema defined in the domain. + +--- + +### app/src/infrastructure/db/repository/locator.js + +```js +const +DbRepository = require('.'), +DbRepositoryMapper = require('./mapper'), +LocatorConstituent = require('superhero/core/locator/constituent') + +class DbRepositoryLocator extends LocatorConstituent +{ + locate() + { + const + gateway = this.locator.locate('db/gateway'), + composer = this.locator.locate('core/schema/composer'), + mapper = new DbRepositoryMapper(composer), + repository = new DbRepository(gateway, mapper) + + return repository + } +} + +module.exports = DbRepositoryLocator +``` + +In the locator for the repository we construct the repository, but we also construct the mapper. The mapper is injected with the located component `core/schema/composer`. + +--- + +### app/src/infrastructure/db/repository/mapper.js + +```js +class DbRepositoryMapper +{ + constructor(composer) + { + this.composer = composer + } + + mapPerson(result) + { + if(result.length < 1) + { + throw new Error('No results found') + } + + if(result.length > 1) + { + throw new Error('Conflicting results found') + } + + return this.composer.compose('entity/person', result[0]) + } +} + +module.exports = DbRepositoryMapper +``` + +In the context-mapper we use the schema composer to validate and filter the result before we return the expected data structure. It is considered good practice to use a composition pattern for the mapper. In this example the mapper is very simple, the mapper only has one responsibility as it is, why I decided to not use a composition pattern in this example. + +--- + +### app/src/infrastructure/db/config.js + +```js +module.exports = +{ + core: + { + locator: + { + 'db/gateway' : __dirname + '/db/gateway', + 'db/repository' : __dirname + '/db/repository' + } + }, + infrastructure: + { + db: + { + gateway: + { + connections : 5, + host : '...', + user : '...', + password : '...', + } + } + } +} +``` + +In the configuration file of the infrastructure we declare the refferences to the location of the services `db/gateway` and `db/repository`. We also define the configuration options to the gateway. The values for `host`, `user` and `password` as 3 dots is placeholders for the values needed in the connection at hand. + +--- + +### app/src/index.js + +```js +const +CoreFactory = require('superhero/core/factory'), +coreFactory = new CoreFactory, +core = coreFactory.create() + +core.add('api') +core.add('domain') +core.add('infrastructure') + +core.load() + +core.locate('core/bootstrap').bootstrap().then(() => +core.locate('core/http/server').listen(80)) +``` + +Finally we define the main script that adds and loads the `api`, `domain` and the `infrastructure` components. After bootstrap, the server is instructed to listen to port `80` for incoming api calls. + +--- + +### app/src/package.json + +```js +{ + "name": "App", + "version": "0.0.1", + "description": "An example app", + "repository": "https://github.com/...", + "license": "MIT", + "main": "src/index.js", + "dependencies": { + "superhero": "*", + "@superhero/db": "*", + "mysql": "*" + } +} +``` + +Below is a simple example of the `package.json` file used in this example application. + +--- + +## Meta definition + +It is possible to define a meta attribute to your schema, which in turn allows 2 different possible meta descriptions; `extends` and `immutable`. + +--- + +### Extends + +From above example, we defined a person in a schema. If we want to have an extended type of a person, a `superhero` which has all the attributes of a person and an additional attribute; `superpower`, then we can use the meta attribute to extend the person as the example below describes. + +```js +module.exports = +{ + '@meta': + { + 'extends' : 'entity/person' + }, + 'superpower': + { + 'type' : 'string', + 'enum' : ['invisibility', 'precognition', 'indestructible'] + } +} +``` + +It is also possible to extend from multiple references by using an array of refferences in the `extends` attribute. + +--- + +### Immutable + +By default, the schema will construct an immutable object, an object that can not be changed, a frozen object. If you like a composed structure to instead be mutable, then you must specify that in the meta attribute, as in the example below. + +```js +module.exports = +{ + '@meta': + { + 'immutable' : false + }, + // ... +} +``` + +--- + +## Dynamic configuration specification + +To make it easier to load schemas, it is possible to use a dynamic reference in the configurations. The example below shows a directory with many different shemas. + +``` +app +└── src + └── domain + ├── schema + │ ├── foo.js + │ ├── bar.js + │ ├── baz.js + │ └── qux.js + └── config.js +``` + +To include all these services, the configuration in the domain is expected to look something like the example below shows. + +```js +module.exports = +{ + core: + { + schema: + { + composer: + { + 'schema/foo' : __dirname + '/schema/foo', + 'schema/bar' : __dirname + '/schema/bar', + 'schema/baz' : __dirname + '/schema/baz', + 'schema/qux' : __dirname + '/schema/qux' + } + } + } +} +``` + +By using an **asterix** `*` in the configuration defintion, to specify a dynamic reference, the same configuration could look like the following example shows. + +```js +module.exports = +{ + core: + { + schema: + { + composer: + { + 'schema/*' : __dirname + '/schema/*' + } + } + } +} +``` + +Both examples above has the same effect. diff --git a/doc/book/1-core-components/string.md b/doc/book/1-core-components/string.md index ed9eff5..6f0b712 100644 --- a/doc/book/1-core-components/string.md +++ b/doc/book/1-core-components/string.md @@ -1,39 +1,39 @@ -# String - -The `core/string` component is an aggregated instance that exposes functionality that operates in relation to the javascript `string` type. - ---- - -### `composeFirstUpperCase` - -This method will make the whole string lowercase, except for the first letter that will be composed as uppercase. - -```js -core.locate('core/string').composeFirstUpperCase('fooBar') -// > 'Foobar' -``` - ---- - -### `composeSeperatedLowerCase` - -Composes a lower cased string that is seperated by defined seperator. - -```js -core.locate('core/string').composeSeperatedLowerCase('Foo BAR baz') -// > 'foo-bar-baz' - -core.locate('core/string').composeSeperatedLowerCase('Foo BAR baz', '.') -// > 'foo.bar.baz' -``` - ---- - -### `composeCamelCase` - -Will compose a string that is lowercased string where each segment is uppercased, then glued together with no seperation, concatting the segments to one camelcased segment. - -```js -core.locate('core/string').composeCamelCase('Foo BAR baz') -// > 'fooBarBaz' -``` +# String + +The `core/string` component is an aggregated instance that exposes functionality that operates in relation to the javascript `string` type. + +--- + +### `composeFirstUpperCase` + +This method will make the whole string lowercase, except for the first letter that will be composed as uppercase. + +```js +core.locate('core/string').composeFirstUpperCase('fooBar') +// > 'Foobar' +``` + +--- + +### `composeSeperatedLowerCase` + +Composes a lower cased string that is seperated by defined seperator. + +```js +core.locate('core/string').composeSeperatedLowerCase('Foo BAR baz') +// > 'foo-bar-baz' + +core.locate('core/string').composeSeperatedLowerCase('Foo BAR baz', '.') +// > 'foo.bar.baz' +``` + +--- + +### `composeCamelCase` + +Will compose a string that is lowercased string where each segment is uppercased, then glued together with no seperation, concatting the segments to one camelcased segment. + +```js +core.locate('core/string').composeCamelCase('Foo BAR baz') +// > 'fooBarBaz' +``` diff --git a/doc/book/2-code-quality/boundaries/layers.md b/doc/book/2-code-quality/boundaries/layers.md index de0d182..a636ea1 100644 --- a/doc/book/2-code-quality/boundaries/layers.md +++ b/doc/book/2-code-quality/boundaries/layers.md @@ -1,23 +1,23 @@ -# Code structure / Boundary / Layers - -On a lower level it makes sense to segragate by layer - -On a policy level it makes sense to segragate by layer - -on an implementaion level it make sense to segrage code by layers to be able to resuse implemnational specific code. - ---- - -## API - ---- - -## Domain - ---- - -## Infrastructure - ---- - -## View +# Code structure / Boundary / Layers + +On a lower level it makes sense to segragate by layer + +On a policy level it makes sense to segragate by layer + +on an implementaion level it make sense to segrage code by layers to be able to resuse implemnational specific code. + +--- + +## API + +--- + +## Domain + +--- + +## Infrastructure + +--- + +## View diff --git a/doc/book/2-code-quality/boundaries/networks.md b/doc/book/2-code-quality/boundaries/networks.md index e016f28..2254d3c 100644 --- a/doc/book/2-code-quality/boundaries/networks.md +++ b/doc/book/2-code-quality/boundaries/networks.md @@ -1,3 +1,3 @@ -# Code structure / Boundary / Networks - -Each interacting component should be bounded in a network. By defining a network by name, and constraining communication outside that scope, security is imporoved, and defines what components are of greater risk in scale to the system if they where to be corrupt or jepordised by a malicious force. +# Code structure / Boundary / Networks + +Each interacting component should be bounded in a network. By defining a network by name, and constraining communication outside that scope, security is imporoved, and defines what components are of greater risk in scale to the system if they where to be corrupt or jepordised by a malicious force. diff --git a/doc/book/2-code-quality/code-structure.md b/doc/book/2-code-quality/code-structure.md index 3ed0417..4621979 100644 --- a/doc/book/2-code-quality/code-structure.md +++ b/doc/book/2-code-quality/code-structure.md @@ -1,74 +1,74 @@ -# Code structure - -Code structure can be modeled in many different ways. Advocated advice expressed in this documentation are guidelines, not enforced or strict rules a developer must submit to. There are many possible models to a problem. The suggested guidelines in this documentation is aimed to improve your ability to structure code in a coherent manner. - ---- - -## Coherent design - -When designing a system of any form, it is of great importance to follow a coherent pattern that is repeated through different resolutions of the system; *much like a mathematical fractal. Just as a fractal, the sequence changes in relation to the surrounding, but a pattern persists. The coherent pattern in different resolutions can be different, though never chaotic.* - -In relation to coherence, chaos is the definition of incoherent. An incoherent system design is hard to understand and navigate through. In mathematics - chaos theory; chaos is defined as an initial small conditional change that has a staggering systematic effect and is hard to predict. When a model of a problem is incoherent, not aligned with pre-defined principles, then it is harder to evaluate what effect a modification to the system has. - -### **Stable design** - -Robert C. Martin pointed out that stability in system architecture is correlated with code that is closed to modification. In one of Roberts great books; *"Clean Architecture: A Craftsman's Guide to Software Structure and Design"*, Robert talks about the "Open / Close" principle - conceived by Bertrand Meyer. The principle expresses that a stable component should be open for extension, and closed to modification. As the principles reflects, extended functionality over modified functionality must be prioritized when possible. - -### **Why a coherent design is important** - -A coherent design eases the ability for developers in a system to navigate the logic the system is composed by. It is important to ease the developers navigational abilities to increase developers ability to maintain the system, to find problems in the system and solve these problems. - -A coherent design is closely related to simplicity. A developer that is developing an operation in one part of the system will in the future need to integrate with, and work in, other parts of the system. When a developer work within different segments of the system, then a developers familiarity in each segment will reflect on the developers agile ability; meaning their possibility to move between projects. - -A coherent design eases the ability to understand the system at scale. Successful systems grows beyond intention; that's an indisputable fact. The result is an increasing complexity, no matter how well designed the system is. An increased complexity generates unexpected behaviors that must be analyzed to solve. A greater complexity results in greater costs. A greater understanding of the system will result in less costs to solve the problems as they occur. - ---- - -## Boundaries - -> *Software architecture is the art of drawing lines* -> **Robert C. Martin** - -A boundary is an encapsulation of components that are tighter coupled then components across the boundaries. Interaction across boundaries are allowed, though they should be limited. Each drawn boundary should define a public interface to be able to interact with the context. When documenting code, priority should be on documenting these interfaces. As there are different levels of implementations, the priority should be to document the higher level implementations. - -It is common to draw boundaries that relates to the code level. A higher level can be distinguished by the resolution of the solution. A "lower level" refers to a deeper resolution, and is closer to the code implementation. An aggregated context is a boundary of a higher level component. - -Eric Evans wrote a great book called *"Domain-Driven Design: Tackling Complexity in the Heart of Software"*. In the book, Evans describes the concept of a bounded context. A bounded context is a good example of an encapsulation of components that separates different domains by explicit boundaries. - -Boundaries within a system is defined to break down a monolithic code base. Boundaries aggregates utility and defines a less noisy dependency map that is easier to understand and navigate. - -### **Dependency direction** - -Each boundary has a dependency direction that determines how the system interacts with other components of the system. The dependency direction should relate to the implementation level, where a lower level is closer to code implementation, and where a lower level of code implementation is closer to IO operations. Components dependency direction should be from higher to lower level. A stronger architecture focuses on stability in the lower level components, where stability is defined by independent components that are closed for modification. - ---- - -## Naming convention - -The naming convention advocated by this documentation should reflect both a domain specific language, as well as a logical specification associated with the named component. - -For example, a class that is part of the `Infrastructure` layer, specifying a `Repository` to the service `Foo` for the resource `Bar`, should be named accordingly; `Infrastructure.FooReposityBar`, where `Infrastructure` is a namespace, and where the class name `FooReposityBar` reflects the directory structure. - -In the named repository, the methods of the class are named after what they do. A method that fetches a resource by id should be named `fetchById`. - -Alternatively, if the repository is aggregated, the naming convention could also look something like; `Infrastructure.FooReposity#fetchBarById`. - -Be explicit in your naming. For example, if you name a query, instead of calling the query `fetch-a-resource`, a more explicit name is `fetch-a-resource-by-id`. - ---- - -## Semantics - -Semantic code is self documented and self explainable. Code that explains it self must cover **what** the code does, **how** the code does what it does and **when** the code does what it does. Using a domain language for semantic definition is an approach that offers a meaning from domain experts perspective. In a higher resolution, at a lower level, technical details are of greater value. From the perspective of an implementer, the domain language has value, but so does a logical definition of **how** the problem has been solved. The semantic model should therefor reflect a domain specific language as well as being defined by logical concepts. - -Eric Evans express the importance of defining a ubiquitous language - a domain specific language that is recognizable by the stakeholders. Such an approach has great value to the modeled solution. The stakeholders are able to have a conversation with the technical team about the different components the solution is composed by, improving collaboration across departments. - -### **Perspective** - -Code structure will be viewed from different perspectives, where different level of implementation has value. - -To grasp an overview of what the implementation does, it is helpful with an aggregated definition that can be unfold and describe a deeper meaning by diving deeper into the resolution. **A top-down perspective.** - -From a maintainers perspective, the implementation details of a solution has value, as well as the effect modification could have upstream to dependent components. **A bottom-up perspective.** - -This documentation advocates that both a **top-down** and **bottom-up** perspectives is reflected in the code structure and designed solution. +# Code structure + +Code structure can be modeled in many different ways. Advocated advice expressed in this documentation are guidelines, not enforced or strict rules a developer must submit to. There are many possible models to a problem. The suggested guidelines in this documentation is aimed to improve your ability to structure code in a coherent manner. + +--- + +## Coherent design + +When designing a system of any form, it is of great importance to follow a coherent pattern that is repeated through different resolutions of the system; *much like a mathematical fractal. Just as a fractal, the sequence changes in relation to the surrounding, but a pattern persists. The coherent pattern in different resolutions can be different, though never chaotic.* + +In relation to coherence, chaos is the definition of incoherent. An incoherent system design is hard to understand and navigate through. In mathematics - chaos theory; chaos is defined as an initial small conditional change that has a staggering systematic effect and is hard to predict. When a model of a problem is incoherent, not aligned with pre-defined principles, then it is harder to evaluate what effect a modification to the system has. + +### **Stable design** + +Robert C. Martin pointed out that stability in system architecture is correlated with code that is closed to modification. In one of Roberts great books; *"Clean Architecture: A Craftsman's Guide to Software Structure and Design"*, Robert talks about the "Open / Close" principle - conceived by Bertrand Meyer. The principle expresses that a stable component should be open for extension, and closed to modification. As the principles reflects, extended functionality over modified functionality must be prioritized when possible. + +### **Why a coherent design is important** + +A coherent design eases the ability for developers in a system to navigate the logic the system is composed by. It is important to ease the developers navigational abilities to increase developers ability to maintain the system, to find problems in the system and solve these problems. + +A coherent design is closely related to simplicity. A developer that is developing an operation in one part of the system will in the future need to integrate with, and work in, other parts of the system. When a developer work within different segments of the system, then a developers familiarity in each segment will reflect on the developers agile ability; meaning their possibility to move between projects. + +A coherent design eases the ability to understand the system at scale. Successful systems grows beyond intention; that's an indisputable fact. The result is an increasing complexity, no matter how well designed the system is. An increased complexity generates unexpected behaviors that must be analyzed to solve. A greater complexity results in greater costs. A greater understanding of the system will result in less costs to solve the problems as they occur. + +--- + +## Boundaries + +> *Software architecture is the art of drawing lines* +> **Robert C. Martin** + +A boundary is an encapsulation of components that are tighter coupled then components across the boundaries. Interaction across boundaries are allowed, though they should be limited. Each drawn boundary should define a public interface to be able to interact with the context. When documenting code, priority should be on documenting these interfaces. As there are different levels of implementations, the priority should be to document the higher level implementations. + +It is common to draw boundaries that relates to the code level. A higher level can be distinguished by the resolution of the solution. A "lower level" refers to a deeper resolution, and is closer to the code implementation. An aggregated context is a boundary of a higher level component. + +Eric Evans wrote a great book called *"Domain-Driven Design: Tackling Complexity in the Heart of Software"*. In the book, Evans describes the concept of a bounded context. A bounded context is a good example of an encapsulation of components that separates different domains by explicit boundaries. + +Boundaries within a system is defined to break down a monolithic code base. Boundaries aggregates utility and defines a less noisy dependency map that is easier to understand and navigate. + +### **Dependency direction** + +Each boundary has a dependency direction that determines how the system interacts with other components of the system. The dependency direction should relate to the implementation level, where a lower level is closer to code implementation, and where a lower level of code implementation is closer to IO operations. Components dependency direction should be from higher to lower level. A stronger architecture focuses on stability in the lower level components, where stability is defined by independent components that are closed for modification. + +--- + +## Naming convention + +The naming convention advocated by this documentation should reflect both a domain specific language, as well as a logical specification associated with the named component. + +For example, a class that is part of the `Infrastructure` layer, specifying a `Repository` to the service `Foo` for the resource `Bar`, should be named accordingly; `Infrastructure.FooReposityBar`, where `Infrastructure` is a namespace, and where the class name `FooReposityBar` reflects the directory structure. + +In the named repository, the methods of the class are named after what they do. A method that fetches a resource by id should be named `fetchById`. + +Alternatively, if the repository is aggregated, the naming convention could also look something like; `Infrastructure.FooReposity#fetchBarById`. + +Be explicit in your naming. For example, if you name a query, instead of calling the query `fetch-a-resource`, a more explicit name is `fetch-a-resource-by-id`. + +--- + +## Semantics + +Semantic code is self documented and self explainable. Code that explains it self must cover **what** the code does, **how** the code does what it does and **when** the code does what it does. Using a domain language for semantic definition is an approach that offers a meaning from domain experts perspective. In a higher resolution, at a lower level, technical details are of greater value. From the perspective of an implementer, the domain language has value, but so does a logical definition of **how** the problem has been solved. The semantic model should therefor reflect a domain specific language as well as being defined by logical concepts. + +Eric Evans express the importance of defining a ubiquitous language - a domain specific language that is recognizable by the stakeholders. Such an approach has great value to the modeled solution. The stakeholders are able to have a conversation with the technical team about the different components the solution is composed by, improving collaboration across departments. + +### **Perspective** + +Code structure will be viewed from different perspectives, where different level of implementation has value. + +To grasp an overview of what the implementation does, it is helpful with an aggregated definition that can be unfold and describe a deeper meaning by diving deeper into the resolution. **A top-down perspective.** + +From a maintainers perspective, the implementation details of a solution has value, as well as the effect modification could have upstream to dependent components. **A bottom-up perspective.** + +This documentation advocates that both a **top-down** and **bottom-up** perspectives is reflected in the code structure and designed solution. diff --git a/doc/book/2-code-quality/domain.md b/doc/book/2-code-quality/domain.md index 5205c4f..d83f338 100644 --- a/doc/book/2-code-quality/domain.md +++ b/doc/book/2-code-quality/domain.md @@ -1,81 +1,81 @@ -# Domain - -A `domain` specifies a solution to a problem that has a boundary drawn around it. The domain is the core of a `bounded context`. A domain is a grouped functionality that has a contextual coherence. Grouping logic by functionality is often considered a good practice. Drawing boundaries around grouped functionality is beneficial to be able to abstract and decrease distracting lower level noise; which improves the ability to control communication patterns, navigation and maintenance work. - -## Breaking down the monolith - -A solution that has a poor dependency map can be related to as a monolith. A monolith is a solution that is big and hard to maintain. "Breaking down the monolith" refers to drawing boundaries in the code, slicing the monolith into pieces that are easier to maintain, extend and reuse. - -In the **DDD** community - **D**omain **D**riven **D**esign, "breaking down the monolith" relates in parts to breaking down the system into "bounded contexts". A `bounded context` is a subset of the system. - -A system is a solution to a problem. When ever a problem needs to be solved, it is beneficial to break the problem down into contextual subsets. By breaking down a problem it is easier to focus on each contextual subset and optimize the solution. - -A broken down monolith is a system that is composed by multiple units that all addresses specific problems in the final solution. *This approach to architecture is a top-down approach; first define a problem, then break that problem down.* - -### Bounded context - -A `bounded context` is autonomous, where alteration within one `bounded context` do not effect another, apart from the obvious effect that changes in a public interface has to dependent components. - -A `bounded context` is an abstract definition, where each implementation is a `core domain` or a `sub domain`. A `sub domain` is also an abstract definition, where each implementation is either a `supporting sub domain` or a `generic sub domain`. - -A `bounded context` is arguable best defined by an operational context. An operational context can best be described by **who** is operating in the context. - -#### `core domain` - -> ... - -#### `supporting sub domain` - -> ... - -#### `generic sub domain` - -> ... - -### A contextual resource - -A contextual resource is a concept that refers to a specific perspective of a data model. In a business, a resource, such as an "order", is often modeled differently in different parts of a system. For example, to an ISP - **I**nternet **S**ervice **P**rovider, an "order" is expected to hold relevant data depending on the context of the `domain`. - -- An "order" to the **technicians** is expected to hold relevant provisioning data -- An "order" to the **marketing** department is expected to hold relevant analytical data -- An "order" to the **finance** department is expected to hold relevant billing details data - -The context of the order plays a role in the definition of what data or functionality an order is expected to have. An order does obviously share an abstract common definition across contexts, but each context does not need to know of operations present to another contextual scope. - -## DDD and Microservices - -Microservice architecture addresses the problem of "breaking down the monolith" by decomposing a system to separate smaller and isolated services, modulating each solution to more reusable, easier to test and more stable components. - -The same reasoning used to advocate a "microservice" architecture is used by the DDD community. A symbiotic branch between microservices and DDD has taken form, where a microservice is declared as a `bounded context`. The approach to define a microservice as a `bounded context` implies a larger microservice then what is commonly argued in favor of in a microservice architecture. - -The benefit of applying both a DDD and microservice architecture in the same stack relates to the inherited value of both approaches, solving the problems appearing in the other methodology. - -### A DDD architecture benefits from a microservice approach - -In a DDD architecture it can be hard to isolate the different contexts, with a clear interface and a proper anti-corruption layer between the domains. - -When relying on support from a microservice architecture with DDD, the distinguished segregation between domains is explicit. The symbolism in packed and separately deployed services as a distinct process, conceptualizes the important notation of isolated domains. - -### A microservice architecture benefits from DDD - -In a microservice architecture, it is often a relate-able problem that components grow tighter coupled into a monolithic network of services. - -By representing a service as a `bounded context`, a boundary is drawn around contexts, resulting in an additional level of resolution to the architecture, clearing out noise in the communication patterns. - -### Branch or leaf - -A leaf service is responsible for a single source of data. - -A branch operates as an aggregated service, responsible for domain logic that is aggregating one or many infrastructures. - -It is considered an anti-pattern, by this documentation, if the responsibility of a leaf and branch service is designed to operate in the same process. *To be, or not to be - an aggregate or not - a binary decision.* - -## Anti corruption layer - -Between different domains, in the infrastructure layer, it is suitable to translate messages from external contexts to the domain language of the operating context. - -An example is when retrieving a DTO, **D**ata **T**ransfer **O**bject, from an external resource. The fetched DTO should be **mapped** to a recognizable data structure to the contextual domain. A recognizable data structure could be an event, entity or a simple value-object. - -Where the mapping from an abstract DTO to a concrete representation happens, is referred to as an anti-corruption layer. In DDD, the anti-corruption layer is represented by a component called "Context-Mapper". - -The responsibility of an anti-corruption layer is to validate, translate, filter and map communication between domains. The segregation of this specific logic has great +# Domain + +A `domain` specifies a solution to a problem that has a boundary drawn around it. The domain is the core of a `bounded context`. A domain is a grouped functionality that has a contextual coherence. Grouping logic by functionality is often considered a good practice. Drawing boundaries around grouped functionality is beneficial to be able to abstract and decrease distracting lower level noise; which improves the ability to control communication patterns, navigation and maintenance work. + +## Breaking down the monolith + +A solution that has a poor dependency map can be related to as a monolith. A monolith is a solution that is big and hard to maintain. "Breaking down the monolith" refers to drawing boundaries in the code, slicing the monolith into pieces that are easier to maintain, extend and reuse. + +In the **DDD** community - **D**omain **D**riven **D**esign, "breaking down the monolith" relates in parts to breaking down the system into "bounded contexts". A `bounded context` is a subset of the system. + +A system is a solution to a problem. When ever a problem needs to be solved, it is beneficial to break the problem down into contextual subsets. By breaking down a problem it is easier to focus on each contextual subset and optimize the solution. + +A broken down monolith is a system that is composed by multiple units that all addresses specific problems in the final solution. *This approach to architecture is a top-down approach; first define a problem, then break that problem down.* + +### Bounded context + +A `bounded context` is autonomous, where alteration within one `bounded context` do not effect another, apart from the obvious effect that changes in a public interface has to dependent components. + +A `bounded context` is an abstract definition, where each implementation is a `core domain` or a `sub domain`. A `sub domain` is also an abstract definition, where each implementation is either a `supporting sub domain` or a `generic sub domain`. + +A `bounded context` is arguable best defined by an operational context. An operational context can best be described by **who** is operating in the context. + +#### `core domain` + +> ... + +#### `supporting sub domain` + +> ... + +#### `generic sub domain` + +> ... + +### A contextual resource + +A contextual resource is a concept that refers to a specific perspective of a data model. In a business, a resource, such as an "order", is often modeled differently in different parts of a system. For example, to an ISP - **I**nternet **S**ervice **P**rovider, an "order" is expected to hold relevant data depending on the context of the `domain`. + +- An "order" to the **technicians** is expected to hold relevant provisioning data +- An "order" to the **marketing** department is expected to hold relevant analytical data +- An "order" to the **finance** department is expected to hold relevant billing details data + +The context of the order plays a role in the definition of what data or functionality an order is expected to have. An order does obviously share an abstract common definition across contexts, but each context does not need to know of operations present to another contextual scope. + +## DDD and Microservices + +Microservice architecture addresses the problem of "breaking down the monolith" by decomposing a system to separate smaller and isolated services, modulating each solution to more reusable, easier to test and more stable components. + +The same reasoning used to advocate a "microservice" architecture is used by the DDD community. A symbiotic branch between microservices and DDD has taken form, where a microservice is declared as a `bounded context`. The approach to define a microservice as a `bounded context` implies a larger microservice then what is commonly argued in favor of in a microservice architecture. + +The benefit of applying both a DDD and microservice architecture in the same stack relates to the inherited value of both approaches, solving the problems appearing in the other methodology. + +### A DDD architecture benefits from a microservice approach + +In a DDD architecture it can be hard to isolate the different contexts, with a clear interface and a proper anti-corruption layer between the domains. + +When relying on support from a microservice architecture with DDD, the distinguished segregation between domains is explicit. The symbolism in packed and separately deployed services as a distinct process, conceptualizes the important notation of isolated domains. + +### A microservice architecture benefits from DDD + +In a microservice architecture, it is often a relate-able problem that components grow tighter coupled into a monolithic network of services. + +By representing a service as a `bounded context`, a boundary is drawn around contexts, resulting in an additional level of resolution to the architecture, clearing out noise in the communication patterns. + +### Branch or leaf + +A leaf service is responsible for a single source of data. + +A branch operates as an aggregated service, responsible for domain logic that is aggregating one or many infrastructures. + +It is considered an anti-pattern, by this documentation, if the responsibility of a leaf and branch service is designed to operate in the same process. *To be, or not to be - an aggregate or not - a binary decision.* + +## Anti corruption layer + +Between different domains, in the infrastructure layer, it is suitable to translate messages from external contexts to the domain language of the operating context. + +An example is when retrieving a DTO, **D**ata **T**ransfer **O**bject, from an external resource. The fetched DTO should be **mapped** to a recognizable data structure to the contextual domain. A recognizable data structure could be an event, entity or a simple value-object. + +Where the mapping from an abstract DTO to a concrete representation happens, is referred to as an anti-corruption layer. In DDD, the anti-corruption layer is represented by a component called "Context-Mapper". + +The responsibility of an anti-corruption layer is to validate, translate, filter and map communication between domains. The segregation of this specific logic has great diff --git a/doc/book/3-code-automation/generate-api-classes-and-tests.md b/doc/book/3-code-automation/generate-api-classes-and-tests.md index 8784046..085107e 100644 --- a/doc/book/3-code-automation/generate-api-classes-and-tests.md +++ b/doc/book/3-code-automation/generate-api-classes-and-tests.md @@ -1 +1 @@ -# Code automation - Generate API classes and corresponding tests from configuration +# Code automation - Generate API classes and corresponding tests from configuration diff --git a/doc/book/3-code-automation/generate-api-documentation.md b/doc/book/3-code-automation/generate-api-documentation.md index 9e9cb94..429ec4e 100644 --- a/doc/book/3-code-automation/generate-api-documentation.md +++ b/doc/book/3-code-automation/generate-api-documentation.md @@ -1 +1 @@ -# Code automation - Generate API documentation +# Code automation - Generate API documentation diff --git a/doc/book/3-code-automation/generate-domain-classes.md b/doc/book/3-code-automation/generate-domain-classes.md index 5afd87f..7f0368a 100644 --- a/doc/book/3-code-automation/generate-domain-classes.md +++ b/doc/book/3-code-automation/generate-domain-classes.md @@ -1 +1 @@ -# Code automation - Generate Domain classes from configuration +# Code automation - Generate Domain classes from configuration diff --git a/doc/book/3-code-automation/generate-project.md b/doc/book/3-code-automation/generate-project.md index 27a6242..33a8557 100644 --- a/doc/book/3-code-automation/generate-project.md +++ b/doc/book/3-code-automation/generate-project.md @@ -1,179 +1,179 @@ -# Code automation - Generate project - -Bellow is an excerpt of the navigation choice **1. Generate project**. All choices is explained in the end of this documentation. - -``` - - __ - _______ ______ ___ _____/ /_ ___ _________ - / ___/ / / / __ \/ _ \/ ___/ __ \/ _ \/ ___/ __ \ - (__ ) /_/ / /_/ / __/ / / / / / __/ / / /_/ / - /____/\__,_/ .___/\___/_/ /_/ /_/\___/_/ \____/ - /_/ - - -1. Generate project -2. Generate API classes and corresponding tests from configuration -3. Generate API documentation -4. Generate Domain classes from configuration - -Choose your destiny: 1 - ✔ Excellent - -What is the name of the project? test - ✔ Excellent - -How would you describe the project in one sentence? This is simply a test example - ✔ Excellent - -What is the URL to the repository? git@github.com:example/test - ✔ Excellent - -Add "infrastructure" component? no - ✔ Excellent - -Add "view" component? no - ✔ Excellent - -What timezone does the project reflect? CET - ✔ Excellent - -Specify the path to where the project will be generated, or leave blank to use /home/example/Projects/test -Where do you want to generate the project? - ✔ Excellent - - ------------- - ¡ Finish it ! - ------------- - Creating path: src - Creating path: src/api - Creating path: src/domain - Creating path: test - Creating file: src/api/config.js - Creating file: src/domain/config.js - Creating file: src/index.js - Creating file: test/init.js - Creating file: test/mocha.opts - Creating file: .gitignore - Creating file: Dockerfile - Creating file: package.json - Creating file: README.md - ------------- -``` - ---- - -## Generate project - -To start with, the first choice in the main list is selected: **1. Generate project**. This option will automaticly generate the barebone of the project. This option is a bootstrap operation to reduce redundant work necessery to set up basic project structure. - ---- - -## Name of project - -When prompted with the question **What is the name of the project?**, it is expected that a name relevant to the project being created is given. There is no need to format the name specificly, the name is filtered to suit restrictions where the name is used in the different files. - ---- - -## Description of the project - -The question: **How would you describe the project in one sentence?** expects a short description of the project. It is possible to leave the option empty, but it is recomended to give a short description of the core domain of the project. - ---- - -## Version-control repository - -A question regarding the version-control repository is asked next: **What is the URL to the repository?**. The link to the version-control repository can for instance be a github link to a repository you have set up, or expect to set up in the future. - -This step is optional, though encouraged to fulfill as npm will throw warnings if the step is ignored. - ---- - -## Add additional layers - -Some layers of the application is often not required to in every project. For that reason, two questions are prompted regarding the `infrastructure` layer and `view` layer. - ---- - -### Infrastructure layer - -The `infrastructure` layer is expected to contain logic related to comunication between the project and external resources. The option requires a `yes` or `no` answer. - ---- - -### View layer - -The `view` layer is expected to contain the presentation logic of the project. - ---- - -## Timezone - -Next it is time to specify the timezone of the project: **What timezone does the project reflect?**. This step has many different options that all can be viewed through double tapping the tab key. **CET**, that is used in the example, stands for **Central Europe Time**. - ---- - -## Path to the project - -The CLI helper requires an absolute path to the project root folder. An option is provided in the question: **Specify the path to where the project will be generated, or leave blank to use ...** which is generated by concatting the current working directory and the name of the project. As described in the quoted text, you can simply hit the the enter key to use the generated alternative, or write the absolute path to the project to use a different location. - ---- - -## Files generated - -Files generated are all listed in the end of the script. Bellow, each files purpose is described. - ---- - -### `src/api/config.js` - -The configuration file of the `API` layer. - ---- - -### `src/domain/config.js` - -The configuration file of the `Domain` layer. - ---- - -### `src/index.js` - -The `index` file is the entrypoint to the application, where the core context is composed and loaded. - ---- - -### `test/init.js` - -Some inital settings related to the test environment. - ---- - -### `test/mocha.opts` - -All the default options to mocha is defined in a seperate file to avoid redundancy in the different test scripts defined in the `package.json` file. - ---- - -### `.gitignore` - -The `.gitignore` file is used to exclude files and folders from the git repository. - ---- - -### `Dockerfile` - -A dockerfile is used to build an environment which the project can run in. - ---- - -### `package.json` - -The `package.json` file declares project dependencies, settings and entrypoints. - ---- - -### `README.md` - -Every project should have a `readme` file that describes the project at length. The generated file is a simple beginning of further work. - +# Code automation - Generate project + +Bellow is an excerpt of the navigation choice **1. Generate project**. All choices is explained in the end of this documentation. + +``` + + __ + _______ ______ ___ _____/ /_ ___ _________ + / ___/ / / / __ \/ _ \/ ___/ __ \/ _ \/ ___/ __ \ + (__ ) /_/ / /_/ / __/ / / / / / __/ / / /_/ / + /____/\__,_/ .___/\___/_/ /_/ /_/\___/_/ \____/ + /_/ + + +1. Generate project +2. Generate API classes and corresponding tests from configuration +3. Generate API documentation +4. Generate Domain classes from configuration + +Choose your destiny: 1 + ✔ Excellent + +What is the name of the project? test + ✔ Excellent + +How would you describe the project in one sentence? This is simply a test example + ✔ Excellent + +What is the URL to the repository? git@github.com:example/test + ✔ Excellent + +Add "infrastructure" component? no + ✔ Excellent + +Add "view" component? no + ✔ Excellent + +What timezone does the project reflect? CET + ✔ Excellent + +Specify the path to where the project will be generated, or leave blank to use /home/example/Projects/test +Where do you want to generate the project? + ✔ Excellent + + ------------- + ¡ Finish it ! + ------------- + Creating path: src + Creating path: src/api + Creating path: src/domain + Creating path: test + Creating file: src/api/config.js + Creating file: src/domain/config.js + Creating file: src/index.js + Creating file: test/init.js + Creating file: test/mocha.opts + Creating file: .gitignore + Creating file: Dockerfile + Creating file: package.json + Creating file: README.md + ------------- +``` + +--- + +## Generate project + +To start with, the first choice in the main list is selected: **1. Generate project**. This option will automaticly generate the barebone of the project. This option is a bootstrap operation to reduce redundant work necessery to set up basic project structure. + +--- + +## Name of project + +When prompted with the question **What is the name of the project?**, it is expected that a name relevant to the project being created is given. There is no need to format the name specificly, the name is filtered to suit restrictions where the name is used in the different files. + +--- + +## Description of the project + +The question: **How would you describe the project in one sentence?** expects a short description of the project. It is possible to leave the option empty, but it is recomended to give a short description of the core domain of the project. + +--- + +## Version-control repository + +A question regarding the version-control repository is asked next: **What is the URL to the repository?**. The link to the version-control repository can for instance be a github link to a repository you have set up, or expect to set up in the future. + +This step is optional, though encouraged to fulfill as npm will throw warnings if the step is ignored. + +--- + +## Add additional layers + +Some layers of the application is often not required to in every project. For that reason, two questions are prompted regarding the `infrastructure` layer and `view` layer. + +--- + +### Infrastructure layer + +The `infrastructure` layer is expected to contain logic related to comunication between the project and external resources. The option requires a `yes` or `no` answer. + +--- + +### View layer + +The `view` layer is expected to contain the presentation logic of the project. + +--- + +## Timezone + +Next it is time to specify the timezone of the project: **What timezone does the project reflect?**. This step has many different options that all can be viewed through double tapping the tab key. **CET**, that is used in the example, stands for **Central Europe Time**. + +--- + +## Path to the project + +The CLI helper requires an absolute path to the project root folder. An option is provided in the question: **Specify the path to where the project will be generated, or leave blank to use ...** which is generated by concatting the current working directory and the name of the project. As described in the quoted text, you can simply hit the the enter key to use the generated alternative, or write the absolute path to the project to use a different location. + +--- + +## Files generated + +Files generated are all listed in the end of the script. Bellow, each files purpose is described. + +--- + +### `src/api/config.js` + +The configuration file of the `API` layer. + +--- + +### `src/domain/config.js` + +The configuration file of the `Domain` layer. + +--- + +### `src/index.js` + +The `index` file is the entrypoint to the application, where the core context is composed and loaded. + +--- + +### `test/init.js` + +Some inital settings related to the test environment. + +--- + +### `test/mocha.opts` + +All the default options to mocha is defined in a seperate file to avoid redundancy in the different test scripts defined in the `package.json` file. + +--- + +### `.gitignore` + +The `.gitignore` file is used to exclude files and folders from the git repository. + +--- + +### `Dockerfile` + +A dockerfile is used to build an environment which the project can run in. + +--- + +### `package.json` + +The `package.json` file declares project dependencies, settings and entrypoints. + +--- + +### `README.md` + +Every project should have a `readme` file that describes the project at length. The generated file is a simple beginning of further work. + diff --git a/doc/book/3-code-automation/index.md b/doc/book/3-code-automation/index.md index 427ab48..e3b31cc 100644 --- a/doc/book/3-code-automation/index.md +++ b/doc/book/3-code-automation/index.md @@ -1,57 +1,57 @@ -# Code automation - -The superhero library come with a CLI helper that can reduce redundant work. Using this helper is not required if you like to work with the library. The helper is only a suggested tool that could serve to aid a developer to hopefully progress productivity. - ---- - -## Installation - -The helper is mainly tested on different Debian based distributions (Linux). The helper probably work on other platforms as well, if you bump into problems, please help out by posting an issue on Github. - -Install the package globally through the following command. - -`npm install -g superhero` - ---- - -## Navigation - -Once the module is installed globally, you should be able to access the CLI helper by running the command `superhero` in the terminal. Below is an example of the expected output of the `superhero` command. - -``` - __ - _______ ______ ___ _____/ /_ ___ _________ - / ___/ / / / __ \/ _ \/ ___/ __ \/ _ \/ ___/ __ \ - (__ ) /_/ / /_/ / __/ / / / / / __/ / / /_/ / - /____/\__,_/ .___/\___/_/ /_/ /_/\___/_/ \____/ - /_/ - - -1. Generate project -2. Generate API classes and corresponding tests from configuration -3. Generate API documentation -4. Generate Domain classes from configuration - -Choose your destiny: -``` - -Different options are presented in a numbered list. You are asked to pick an alternative by writing the number of the navigation choice, followed by the `enter` key that will execute the choice. - -### Autocomplete - -In any form of choice list, it is possible to hit the tab key to autocomplete the option you already started writing. - -### Options hint - -When prompted with a navigation choice, where a choice from a predefined list is expected, it is possible to double-tab to display a list of valid options. - -The list of alternatives is filtered by the defined prefix of the input written before asking for complementary options. - ---- - -Read more about the different options in the main navigation list under one of the following sub-sections. - -1. [Generate project](generate-project.md) -2. [Generate API classes and corresponding tests from configuration](generate-api-classes-and-tests.md) -3. [Generate API documentation](generate-api-documentation.md) -4. [Generate Domain classes from configuration](generate-domain-classes.md) +# Code automation + +The superhero library come with a CLI helper that can reduce redundant work. Using this helper is not required if you like to work with the library. The helper is only a suggested tool that could serve to aid a developer to hopefully progress productivity. + +--- + +## Installation + +The helper is mainly tested on different Debian based distributions (Linux). The helper probably work on other platforms as well, if you bump into problems, please help out by posting an issue on Github. + +Install the package globally through the following command. + +`npm install -g superhero` + +--- + +## Navigation + +Once the module is installed globally, you should be able to access the CLI helper by running the command `superhero` in the terminal. Below is an example of the expected output of the `superhero` command. + +``` + __ + _______ ______ ___ _____/ /_ ___ _________ + / ___/ / / / __ \/ _ \/ ___/ __ \/ _ \/ ___/ __ \ + (__ ) /_/ / /_/ / __/ / / / / / __/ / / /_/ / + /____/\__,_/ .___/\___/_/ /_/ /_/\___/_/ \____/ + /_/ + + +1. Generate project +2. Generate API classes and corresponding tests from configuration +3. Generate API documentation +4. Generate Domain classes from configuration + +Choose your destiny: +``` + +Different options are presented in a numbered list. You are asked to pick an alternative by writing the number of the navigation choice, followed by the `enter` key that will execute the choice. + +### Autocomplete + +In any form of choice list, it is possible to hit the tab key to autocomplete the option you already started writing. + +### Options hint + +When prompted with a navigation choice, where a choice from a predefined list is expected, it is possible to double-tab to display a list of valid options. + +The list of alternatives is filtered by the defined prefix of the input written before asking for complementary options. + +--- + +Read more about the different options in the main navigation list under one of the following sub-sections. + +1. [Generate project](generate-project.md) +2. [Generate API classes and corresponding tests from configuration](generate-api-classes-and-tests.md) +3. [Generate API documentation](generate-api-documentation.md) +4. [Generate Domain classes from configuration](generate-domain-classes.md) diff --git a/doc/book/4-tutorial/index.md b/doc/book/4-tutorial/index.md index af1e3ab..8120422 100644 --- a/doc/book/4-tutorial/index.md +++ b/doc/book/4-tutorial/index.md @@ -1,38 +1,38 @@ -## Designing a microservice - -When designing code, it's often hard to focus on work due to all the noise that surrounds the task. Many stakeholders are asking for futures they consider important, specifications are changing and urgent maintenance tasks breaks concentration. This is the reality that can be hard, if not impossible, to change. By breaking down the task of designing a service into stepped roadmap will help maintain focus and result in a more coherent design across domains. - -The foundation to a good design of any solution starts by locating the events that defines the solution. A good technique to locate the events are by doing an `eventstorming` session, a methodology developed by Alberto Brandolini, inspired by the DDD community. - -Once the events of the microservice has been defined, they must be organized into a process flow. A process is a tree of events with a certain beginning, a root event. Locate the beginning of a process by ordering the events according to when on a timeline they occur. When it becomes apparent where a process begins, and the body of that process has been organized into a tree-like structure of events, then the process flow has formed an aggregation of the domain. - -![in-out process grid](diagram/in-out-process-grid.svg) - -Each process is must be named semanticly correct in relation to the single responsibility the process maintain. Each process is a domain service, the process flow is what we define in the aggregate. - -Some processes are repetitive through the model, creating repetitive noise in the model. Create a seperate named context and define the contextual process flow seperatly, and link it by name in the `in-out process grid`. - -![dry in-out process grid](diagram/in-out-process-grid-dry.svg) - -Each service can often have multiple states of output, due to unexpected failure or other application states. At this stage it is however important to declare an expected successful event to each process. Ignoring all **exceptional events** to the process, an **expected process flow** is defined. - -List the name of all processes and describe on one hand the event that triggers the process, and on the other, the expected resulting event. Declare the data body of these events; expected input, and expected output. - -When modeling data, we begin with the events. The events are expected to transfer data between processes, so all data will be rooted in the events. It will become apparent that some data sets are repetitive, or clearly has an identity; these data sets are most likely entities. Write a UML diagram of the data model; the events, entities and the value objects. - -In parallel to modeling the data model, the infrastructure layer can offer insight to the data model. List all external services that the solution is dependent on. Define each external service as a concrete repository in the UML diagram. Each repository implements different interfaces that describes the available CRUD operations to a resource. - -After designing the domain and the infrastructure layer; it is time to design the API layer. In the API layer, the root events defines the input body. Additionally it's important to declare url structure in the API, http methods and other relative states. - -once starting to develop, start by develping the master branch, which should reflect the **expected process flow**. After the expected process flow has been developed, develop each exceptional event state into a new branch in the VCS (**V**ersioning **C**ontrol **S**ystem). - -## Plan the work - -- Staging and dev environments -- Start from the domain and work yourself out - -## Make time estimations - -- The time estimation is never final until the project is closed for incoming changes -- Each component designed is a card -- Value to highest and lowest cost for each card +## Designing a microservice + +When designing code, it's often hard to focus on work due to all the noise that surrounds the task. Many stakeholders are asking for futures they consider important, specifications are changing and urgent maintenance tasks breaks concentration. This is the reality that can be hard, if not impossible, to change. By breaking down the task of designing a service into stepped roadmap will help maintain focus and result in a more coherent design across domains. + +The foundation to a good design of any solution starts by locating the events that defines the solution. A good technique to locate the events are by doing an `eventstorming` session, a methodology developed by Alberto Brandolini, inspired by the DDD community. + +Once the events of the microservice has been defined, they must be organized into a process flow. A process is a tree of events with a certain beginning, a root event. Locate the beginning of a process by ordering the events according to when on a timeline they occur. When it becomes apparent where a process begins, and the body of that process has been organized into a tree-like structure of events, then the process flow has formed an aggregation of the domain. + +![in-out process grid](diagram/in-out-process-grid.svg) + +Each process is must be named semanticly correct in relation to the single responsibility the process maintain. Each process is a domain service, the process flow is what we define in the aggregate. + +Some processes are repetitive through the model, creating repetitive noise in the model. Create a seperate named context and define the contextual process flow seperatly, and link it by name in the `in-out process grid`. + +![dry in-out process grid](diagram/in-out-process-grid-dry.svg) + +Each service can often have multiple states of output, due to unexpected failure or other application states. At this stage it is however important to declare an expected successful event to each process. Ignoring all **exceptional events** to the process, an **expected process flow** is defined. + +List the name of all processes and describe on one hand the event that triggers the process, and on the other, the expected resulting event. Declare the data body of these events; expected input, and expected output. + +When modeling data, we begin with the events. The events are expected to transfer data between processes, so all data will be rooted in the events. It will become apparent that some data sets are repetitive, or clearly has an identity; these data sets are most likely entities. Write a UML diagram of the data model; the events, entities and the value objects. + +In parallel to modeling the data model, the infrastructure layer can offer insight to the data model. List all external services that the solution is dependent on. Define each external service as a concrete repository in the UML diagram. Each repository implements different interfaces that describes the available CRUD operations to a resource. + +After designing the domain and the infrastructure layer; it is time to design the API layer. In the API layer, the root events defines the input body. Additionally it's important to declare url structure in the API, http methods and other relative states. + +once starting to develop, start by develping the master branch, which should reflect the **expected process flow**. After the expected process flow has been developed, develop each exceptional event state into a new branch in the VCS (**V**ersioning **C**ontrol **S**ystem). + +## Plan the work + +- Staging and dev environments +- Start from the domain and work yourself out + +## Make time estimations + +- The time estimation is never final until the project is closed for incoming changes +- Each component designed is a card +- Value to highest and lowest cost for each card diff --git a/doc/book/index.md b/doc/book/index.md index edd6868..09f7975 100644 --- a/doc/book/index.md +++ b/doc/book/index.md @@ -1,107 +1,107 @@ -# Index - -- Core components - -- - Bootstrap -- - CLI - Command Line Interface -- - Configuration -- - Console -- - Deepclone -- - Deepfind -- - Deepfreeze -- - Deepmerge -- - Eventbus -- - Http -- - - Request -- - - Server -- - Locator -- - - When to use -- - - When not to use -- - Object -- - Path -- - Process -- - Schema -- - String - -- Code structure - -- - Domain -- - - Microservice -- - - Core domain -- - - Sub domains -- - - Branch or leaf domain - -- - Layers -- - - Api -- - - - Endpoint -- - - - Observer -- - - Domain -- - - - Aggregate -- - - - Composite -- - - - Schema -- - - - - DTO -- - - - - - Command -- - - - - - Query -- - - - - - Event -- - - - - Entity -- - - - - Value object -- - - - - Collection -- - - Infrastructure -- - - - Gateway -- - - - Repository -- - - - Anti corruption layer -- - - View -- - - - Template -- - - - Helper - -- - Networks (???) (Grouping the diferent features by networks) - -- Code automisation - Helper -- - Generate project -- - Generate API classes and corresponding tests from configuration -- - Generate API documentation -- - Generate Domain classes from configuration - -- Tutorial -... Todo application - -- Appendix | Architecture - -- - The importance of balance -... Describing the importance of balance, the importance of realization of none abolute implementation of every principle, guidline or recomendation (don't be a slave to another mans word - think for your self, you know your reality better then someone who never has experienced the enviremnt on which you apply the principles) - -- - Communication - Integration pattern between microservices -- - - MVC like pattern -- - - - M = Model = Backend microservices -- - - - V = View = UI microservices -- - - - C = Controller = API microservice -- - - - - Security -- - - - - Point of failure that will take it all down, replicated state to help solve the problem - -- - Designing software -- - - Specifications to code -- - - - Understanding the stakeholders -- - - - DDD - Domain driven design -- - - - - A good technique for understanding and building applications -- - - - - Good defintions of domain and infrastructure components -- - - - - Eventstorming - -- - Eventsource -- - - The importance of an event log -- - - Create a state from the event log and use it as your statefull db - -- - Isolation -- - - Scopes -- - - Globals - -- - Stateless development -... Conditionless -... don't return bool if possible, it will force conditions else where -... Pollymophism - -- - Don't over enginer -... Over enginering, what is the concept of over enginering and how to improve productivity by not trying to solve everything / every edge-case -... balance coding by flexibility and constraintfull to achive an endresult that reflects simplicity -... KISS - Keep It Simple Stupid -... YAGNI - You Aint Gonna Need It -... MVP - Minimal valiable product +# Index + +- Core components + +- - Bootstrap +- - CLI - Command Line Interface +- - Configuration +- - Console +- - Deepclone +- - Deepfind +- - Deepfreeze +- - Deepmerge +- - Eventbus +- - Http +- - - Request +- - - Server +- - Locator +- - - When to use +- - - When not to use +- - Object +- - Path +- - Process +- - Schema +- - String + +- Code structure + +- - Domain +- - - Microservice +- - - Core domain +- - - Sub domains +- - - Branch or leaf domain + +- - Layers +- - - Api +- - - - Endpoint +- - - - Observer +- - - Domain +- - - - Aggregate +- - - - Composite +- - - - Schema +- - - - - DTO +- - - - - - Command +- - - - - - Query +- - - - - - Event +- - - - - Entity +- - - - - Value object +- - - - - Collection +- - - Infrastructure +- - - - Gateway +- - - - Repository +- - - - Anti corruption layer +- - - View +- - - - Template +- - - - Helper + +- - Networks (???) (Grouping the diferent features by networks) + +- Code automisation - Helper +- - Generate project +- - Generate API classes and corresponding tests from configuration +- - Generate API documentation +- - Generate Domain classes from configuration + +- Tutorial +... Todo application + +- Appendix | Architecture + +- - The importance of balance +... Describing the importance of balance, the importance of realization of none abolute implementation of every principle, guidline or recomendation (don't be a slave to another mans word - think for your self, you know your reality better then someone who never has experienced the enviremnt on which you apply the principles) + +- - Communication - Integration pattern between microservices +- - - MVC like pattern +- - - - M = Model = Backend microservices +- - - - V = View = UI microservices +- - - - C = Controller = API microservice +- - - - - Security +- - - - - Point of failure that will take it all down, replicated state to help solve the problem + +- - Designing software +- - - Specifications to code +- - - - Understanding the stakeholders +- - - - DDD - Domain driven design +- - - - - A good technique for understanding and building applications +- - - - - Good defintions of domain and infrastructure components +- - - - - Eventstorming + +- - Eventsource +- - - The importance of an event log +- - - Create a state from the event log and use it as your statefull db + +- - Isolation +- - - Scopes +- - - Globals + +- - Stateless development +... Conditionless +... don't return bool if possible, it will force conditions else where +... Pollymophism + +- - Don't over enginer +... Over enginering, what is the concept of over enginering and how to improve productivity by not trying to solve everything / every edge-case +... balance coding by flexibility and constraintfull to achive an endresult that reflects simplicity +... KISS - Keep It Simple Stupid +... YAGNI - You Aint Gonna Need It +... MVP - Minimal valiable product diff --git a/doc/standard/sop-api.md b/doc/standard/sop-api.md index 324e684..52d3b05 100644 --- a/doc/standard/sop-api.md +++ b/doc/standard/sop-api.md @@ -1,16 +1,16 @@ -_**sop-api**-1.0.0_ -_Author: Erik Landvall_ -# Standard Operating Procedure -### API - -This standard defines a folder structure in the `API` layer. The `API` layer is where the application interfaces are defined. This standard is not exclusive, a developer can chose to extend the structure to cover additional definitions. - -![API diagram](diagram/sop-api.svg) - -An optional `endpoint` folder has a nested file structure of child resources, where each resource is defined by the name of the folder, and each file implementation is named by the version number of implemented release. It's part of the standard to dictate that breaking changes to an endpoint must be defined in a new version, that also must be reflected in the router, configured in the `config` file. Endpoints that are deprecated must respond to requests that the implemented version is deprecated, endpoint should describe how the new endpoint can be used, and document relevant mapping, the difference between the input and output. - -An optional `middleware` folder, contains shared middleware dispatchers that can be applied to the route of all endpoints. The responsibility of a middleware is to address any pre or post actions related to the endpoint. - -An optional `observer` folder, lists folder by event names. All events can be observed by multiple observers, that are listed as siblings under the event named folder. All observers are dependent on the event they are listed under. - -The `config` file is responsible for configuring the router and/or attach listeners for the observers to the core eventbus. +_**sop-api**-1.0.0_ +_Author: Erik Landvall_ +# Standard Operating Procedure +### API + +This standard defines a folder structure in the `API` layer. The `API` layer is where the application interfaces are defined. This standard is not exclusive, a developer can chose to extend the structure to cover additional definitions. + +![API diagram](diagram/sop-api.svg) + +An optional `endpoint` folder has a nested file structure of child resources, where each resource is defined by the name of the folder, and each file implementation is named by the version number of implemented release. It's part of the standard to dictate that breaking changes to an endpoint must be defined in a new version, that also must be reflected in the router, configured in the `config` file. Endpoints that are deprecated must respond to requests that the implemented version is deprecated, endpoint should describe how the new endpoint can be used, and document relevant mapping, the difference between the input and output. + +An optional `middleware` folder, contains shared middleware dispatchers that can be applied to the route of all endpoints. The responsibility of a middleware is to address any pre or post actions related to the endpoint. + +An optional `observer` folder, lists folder by event names. All events can be observed by multiple observers, that are listed as siblings under the event named folder. All observers are dependent on the event they are listed under. + +The `config` file is responsible for configuring the router and/or attach listeners for the observers to the core eventbus. diff --git a/doc/standard/sop-domain.md b/doc/standard/sop-domain.md index c2e4b83..fe8410c 100644 --- a/doc/standard/sop-domain.md +++ b/doc/standard/sop-domain.md @@ -1,28 +1,28 @@ -_**sop-domain**-1.0.0_ -_Author: Erik Landvall_ -# Standard Operating Procedure -### Domain - -This standard is strongly correlated with the guidelines that has grown out of the domain driven design community. - -The domain is segregated into different layers that has a flat file structure, instead of a tree structure, to be able to reuse components in different contexts. A tree model is designed and defined by logic, not by the source codes file structure. - -![Domain diagram](diagram/sop-domain.svg) - -An `aggregate` is responsible for defining the coherence between compositions that defines the process for atomic events related to an aggregate root. An aggregate root is an entity that is related to the aggregate. An entity can be related to other entities, that is to define a logical tree structure. The aggregates responsibility is to associate verbs, that are defined by the stakeholders, and that are implemented in a composite, with the contextual noun; the aggregate root. An aggregate depends on compositions, where the delegated responsibility of the implementation rests. - -The `composite` folder is a flat definition of different domain services. A domain service is related to the verbs defined by the stakeholders. One composite can relate to, or depend on, another composite. A composite is strongly related to the single responsibility principle. - -The `schema` folder houses different schema that are divided into sub types: -- `dto` - the data transfer model, divided into 2 sub types: -- - `command` - a query, something that should be done or carried out, present tense -- - `event` - something that has happen, past tense -- `entity` - correlates with the nouns identified by the stakeholders -- `value-object` - relates to the adjectives identified by the stakeholders -- - `validator` and `filter` - optional sup folders that defines specific logic related to the contextual adjective defined in there parent scope - -The `config` file defines the collected configurations for the domain layer. - ---- - -A global repository should be taken into account for shared resources between different bounded contexts. A linked definition is required where the linked resource is expected to be defined. +_**sop-domain**-1.0.0_ +_Author: Erik Landvall_ +# Standard Operating Procedure +### Domain + +This standard is strongly correlated with the guidelines that has grown out of the domain driven design community. + +The domain is segregated into different layers that has a flat file structure, instead of a tree structure, to be able to reuse components in different contexts. A tree model is designed and defined by logic, not by the source codes file structure. + +![Domain diagram](diagram/sop-domain.svg) + +An `aggregate` is responsible for defining the coherence between compositions that defines the process for atomic events related to an aggregate root. An aggregate root is an entity that is related to the aggregate. An entity can be related to other entities, that is to define a logical tree structure. The aggregates responsibility is to associate verbs, that are defined by the stakeholders, and that are implemented in a composite, with the contextual noun; the aggregate root. An aggregate depends on compositions, where the delegated responsibility of the implementation rests. + +The `composite` folder is a flat definition of different domain services. A domain service is related to the verbs defined by the stakeholders. One composite can relate to, or depend on, another composite. A composite is strongly related to the single responsibility principle. + +The `schema` folder houses different schema that are divided into sub types: +- `dto` - the data transfer model, divided into 2 sub types: +- - `command` - a query, something that should be done or carried out, present tense +- - `event` - something that has happen, past tense +- `entity` - correlates with the nouns identified by the stakeholders +- `value-object` - relates to the adjectives identified by the stakeholders +- - `validator` and `filter` - optional sup folders that defines specific logic related to the contextual adjective defined in there parent scope + +The `config` file defines the collected configurations for the domain layer. + +--- + +A global repository should be taken into account for shared resources between different bounded contexts. A linked definition is required where the linked resource is expected to be defined. diff --git a/doc/standard/sop-event.md b/doc/standard/sop-event.md index 0375388..9c1bbae 100644 --- a/doc/standard/sop-event.md +++ b/doc/standard/sop-event.md @@ -1,12 +1,12 @@ -_**sop-event**-1.0.0_ -_Author: Erik Landvall_ -# Standard Operating Procedure -### Event - -![Event diagram](diagram/sop-event.svg) - -The event structure is defined by 2 layers. - -The `meta` layer is responsible for holding all meta related data to the event, such as the name of the event. - -The `data` layer is the event message. +_**sop-event**-1.0.0_ +_Author: Erik Landvall_ +# Standard Operating Procedure +### Event + +![Event diagram](diagram/sop-event.svg) + +The event structure is defined by 2 layers. + +The `meta` layer is responsible for holding all meta related data to the event, such as the name of the event. + +The `data` layer is the event message. diff --git a/doc/standard/sop-infrastructure.md b/doc/standard/sop-infrastructure.md index c1ed4fb..7213ffb 100644 --- a/doc/standard/sop-infrastructure.md +++ b/doc/standard/sop-infrastructure.md @@ -1,16 +1,16 @@ -_**sop-infrastructure**-1.1.0_ -_Author: Erik Landvall_ -# Standard Operating Procedure -### Infrastructure - -![Infrastructure diagram](diagram/sop-infrastructure.svg) - -This standard defines a folder structure for the infrastructure layer. The infrastructure layer is responsible for the logic that is related to external services. The folder structure defines a list of bundles, that semantically correlates with the external service. Each `bundle` has 2 different sub layers: -- `gateway` - list of drivers necessary to interface with the API of the contextual `bundle` -- `repository` - correlated with external resources, on witch optional reader/writer operations can be defined, though sugested to be seperated in reference to the CQRS pattern. - -The segregation of the read and write models, correlates with the responsibility each layer maintain. Operations are expected to honor the single responsibility principle. - -An operation often requires a `context mapper` that is responsible for mapping data between the present context and the context defined by the `bundle`, or `bounded context`. - -The `config` file defines the collected configurations for all the infrastructure, external services, that the bounded context, that the infrastructure layer partly defines, depends on. +_**sop-infrastructure**-1.1.0_ +_Author: Erik Landvall_ +# Standard Operating Procedure +### Infrastructure + +![Infrastructure diagram](diagram/sop-infrastructure.svg) + +This standard defines a folder structure for the infrastructure layer. The infrastructure layer is responsible for the logic that is related to external services. The folder structure defines a list of bundles, that semantically correlates with the external service. Each `bundle` has 2 different sub layers: +- `gateway` - list of drivers necessary to interface with the API of the contextual `bundle` +- `repository` - correlated with external resources, on witch optional reader/writer operations can be defined, though sugested to be seperated in reference to the CQRS pattern. + +The segregation of the read and write models, correlates with the responsibility each layer maintain. Operations are expected to honor the single responsibility principle. + +An operation often requires a `context mapper` that is responsible for mapping data between the present context and the context defined by the `bundle`, or `bounded context`. + +The `config` file defines the collected configurations for all the infrastructure, external services, that the bounded context, that the infrastructure layer partly defines, depends on. diff --git a/doc/standard/sop-ms.md b/doc/standard/sop-ms.md index df875fd..22f9956 100644 --- a/doc/standard/sop-ms.md +++ b/doc/standard/sop-ms.md @@ -1,34 +1,34 @@ -_**sop-ms**-1.0.0_ -_Author: Erik Landvall_ -# Standard Operating Procedure -### Microservice - -A microservice is defined by its context. A microservice has a responsibility that is implied by the context. A microservice is limited by the barriers that is defined by the microservice responsibility. - -All developers must use and follow this standard for all microservices. Non conflicting extensions to the standard is allowed when necessity to cover exceptional cases persists. - -This standard defines how to structure the code files and folders in a microservice. The responsibility of the standard is to define fundamental levels of a folder structure. Delegated responsibilities are defined in linked standards, described elsewhere. - -![Microservice diagram](diagram/sop-ms.svg) - -This standard is expected to be applied in the root folder of a microservice. - -The `bin` folder contains all exceptional scripts related to the microservice. The `bin` folder is optional to include in a microservice, depending on necessity in relation to the microservice the developer is developing. - -The `doc` folder contains all documentation related to the microservice. The `doc` folder has a child folder called `generated`. The `generated` child folder contains the auto generated documentation that is generated from the written source and test code. - -The `src` folder is where all the source code is stored. Sub-folders: `api`, `domain`, `infrastructure`, `view`; are linked to specific standards related to there respective layers. All microservices must implement an API and a Domain model. The Infrastructure and View models are optionals, depending on the necessity of the microservice. - -The `test` folder is where test specific code is stored. Linked standard `sop-test` defines an expected test structure. - -The `.gitignore` file is required to specify what files we do not want to include in the git repository. - -The `Dockerfile` is required to define the microservice expected environment. Without this specification, external dependencies necessary for the application to run, are left undefined. - -The `README.md` file must describe the responsibility of the bounded context. If this file is not present, it's hard for a developer to concept what logic is expected in the microsevice. - -The `package.json` file is necessary for a node.js application to define dependencies and executable scripts related to the microservice. - ---- - -Attempts to keep the root folder clean, and less noisy, in a microservice is essential for a developers ability to find an initial overview of what the service responsibility is, how the microservice is structured and for a developer to quickly be able to understand the core behavior of the microservice. +_**sop-ms**-1.0.0_ +_Author: Erik Landvall_ +# Standard Operating Procedure +### Microservice + +A microservice is defined by its context. A microservice has a responsibility that is implied by the context. A microservice is limited by the barriers that is defined by the microservice responsibility. + +All developers must use and follow this standard for all microservices. Non conflicting extensions to the standard is allowed when necessity to cover exceptional cases persists. + +This standard defines how to structure the code files and folders in a microservice. The responsibility of the standard is to define fundamental levels of a folder structure. Delegated responsibilities are defined in linked standards, described elsewhere. + +![Microservice diagram](diagram/sop-ms.svg) + +This standard is expected to be applied in the root folder of a microservice. + +The `bin` folder contains all exceptional scripts related to the microservice. The `bin` folder is optional to include in a microservice, depending on necessity in relation to the microservice the developer is developing. + +The `doc` folder contains all documentation related to the microservice. The `doc` folder has a child folder called `generated`. The `generated` child folder contains the auto generated documentation that is generated from the written source and test code. + +The `src` folder is where all the source code is stored. Sub-folders: `api`, `domain`, `infrastructure`, `view`; are linked to specific standards related to there respective layers. All microservices must implement an API and a Domain model. The Infrastructure and View models are optionals, depending on the necessity of the microservice. + +The `test` folder is where test specific code is stored. Linked standard `sop-test` defines an expected test structure. + +The `.gitignore` file is required to specify what files we do not want to include in the git repository. + +The `Dockerfile` is required to define the microservice expected environment. Without this specification, external dependencies necessary for the application to run, are left undefined. + +The `README.md` file must describe the responsibility of the bounded context. If this file is not present, it's hard for a developer to concept what logic is expected in the microsevice. + +The `package.json` file is necessary for a node.js application to define dependencies and executable scripts related to the microservice. + +--- + +Attempts to keep the root folder clean, and less noisy, in a microservice is essential for a developers ability to find an initial overview of what the service responsibility is, how the microservice is structured and for a developer to quickly be able to understand the core behavior of the microservice. diff --git a/doc/standard/sop-src.md b/doc/standard/sop-src.md index 31c0142..16e3242 100644 --- a/doc/standard/sop-src.md +++ b/doc/standard/sop-src.md @@ -1,24 +1,24 @@ -_**sop-src**-1.0.0_ -_Author: Erik Landvall_ -# Standard Operating Procedure -### Source code - -This standard addresses direction of dependencies between different layers of the source code. This standard is strict and must be followed with no exceptions. - -![Source code diagram](diagram/sop-src.svg) - -This standard is expected to be applied on the source code that defines the logic of a service. - -As the diagram reveals; the `API` layer, knows of and can have dependencies from, the `Infrastructure`, `Domain` and the `View` layer, as well as from the `API` layer it self. The `API` is expected to only know of the `view-model` in the `View` layer. - -The `Infrastructure` layer only knows of the `Domain` layer, and can therefor only have dependencies from the `Domain` layer or other parts of the `Infrastructure` layer. - -The `Domain` layer knows of it self and nothing outside its own context. Implementations in the `Domain` layer can only depend on definitions that persists within its layer. The sub-layer `schema` is explicitly expressed for this standard to be able to define the restricted access of the `view-model` sub-layer into the `Domain` layer. - -The `View` possess a sub-layer called `view-model`, expressed specifically in the diagram to express the restricted knowledge the `API` layers has of the `View` layer. The `View` layer does not have knowledge to anything outside its layer, by design. The idea is for the `View` layer to be as dumb as possible; meaning, less logic in the view is encourage. - -This standard implies that the API has a responsibility to eager load all data the view requires. - ---- - -Often we notice that implementations in one layer require a knowledge of a layer it has no access to, in accordance with this standard. A concrete example is the common problem with a domain instance that require knowledge of infrastructure logic. To solve a dependency from the domain to the infrastructure, the dependency inversion principle, part of the SOLID principles authored by Robert C. Martin, can be applied. An interface must be defined in the domain layer that instances in the infrastructure implements, and that other actors in the domain can depend on. +_**sop-src**-1.0.0_ +_Author: Erik Landvall_ +# Standard Operating Procedure +### Source code + +This standard addresses direction of dependencies between different layers of the source code. This standard is strict and must be followed with no exceptions. + +![Source code diagram](diagram/sop-src.svg) + +This standard is expected to be applied on the source code that defines the logic of a service. + +As the diagram reveals; the `API` layer, knows of and can have dependencies from, the `Infrastructure`, `Domain` and the `View` layer, as well as from the `API` layer it self. The `API` is expected to only know of the `view-model` in the `View` layer. + +The `Infrastructure` layer only knows of the `Domain` layer, and can therefor only have dependencies from the `Domain` layer or other parts of the `Infrastructure` layer. + +The `Domain` layer knows of it self and nothing outside its own context. Implementations in the `Domain` layer can only depend on definitions that persists within its layer. The sub-layer `schema` is explicitly expressed for this standard to be able to define the restricted access of the `view-model` sub-layer into the `Domain` layer. + +The `View` possess a sub-layer called `view-model`, expressed specifically in the diagram to express the restricted knowledge the `API` layers has of the `View` layer. The `View` layer does not have knowledge to anything outside its layer, by design. The idea is for the `View` layer to be as dumb as possible; meaning, less logic in the view is encourage. + +This standard implies that the API has a responsibility to eager load all data the view requires. + +--- + +Often we notice that implementations in one layer require a knowledge of a layer it has no access to, in accordance with this standard. A concrete example is the common problem with a domain instance that require knowledge of infrastructure logic. To solve a dependency from the domain to the infrastructure, the dependency inversion principle, part of the SOLID principles authored by Robert C. Martin, can be applied. An interface must be defined in the domain layer that instances in the infrastructure implements, and that other actors in the domain can depend on. diff --git a/doc/standard/sop-test.md b/doc/standard/sop-test.md index f026f08..d3e6331 100644 --- a/doc/standard/sop-test.md +++ b/doc/standard/sop-test.md @@ -1,10 +1,10 @@ -_**sop-test**-1.0.0_ -_Author: Erik Landvall_ -# Standard Operating Procedure -### Test - -![Test diagram](diagram/sop-test.svg) - -This standard defines an expected test file structure. - -The `integration` layer is expected to list a test case for each endpoint. Each test file must be declared with the suffix `test`. +_**sop-test**-1.0.0_ +_Author: Erik Landvall_ +# Standard Operating Procedure +### Test + +![Test diagram](diagram/sop-test.svg) + +This standard defines an expected test file structure. + +The `integration` layer is expected to list a test case for each endpoint. Each test file must be declared with the suffix `test`. diff --git a/doc/standard/sop-view.md b/doc/standard/sop-view.md index 1d9e4e8..b4faf03 100644 --- a/doc/standard/sop-view.md +++ b/doc/standard/sop-view.md @@ -1,14 +1,14 @@ -_**sop-view**-1.0.0_ -_Author: Erik Landvall_ -# Standard Operating Procedure -### View - -![View diagram](diagram/sop-view.svg) - -The optional `helper` folder is a flat file structure of helpers related to the template engine. - -The optional `template` folder lists template scripts necessary for the view to compose the output. - -The optional `view-model` folder lists schemas that are important for validation, before the view model is used by the template engine. - -The `config` file is primarily responsible for attaching listeners and configure view related actors. +_**sop-view**-1.0.0_ +_Author: Erik Landvall_ +# Standard Operating Procedure +### View + +![View diagram](diagram/sop-view.svg) + +The optional `helper` folder is a flat file structure of helpers related to the template engine. + +The optional `template` folder lists template scripts necessary for the view to compose the output. + +The optional `view-model` folder lists schemas that are important for validation, before the view model is used by the template engine. + +The `config` file is primarily responsible for attaching listeners and configure view related actors. diff --git a/index.js b/index.js index cfe1294..c37ea7a 100644 --- a/index.js +++ b/index.js @@ -1,226 +1,226 @@ -const fs = require('fs') - -class Core -{ - constructor(locator) - { - this.locator = locator - this.components = {} - } - - add(component, pathname) - { - this.components[component] = pathname - } - - remove(component) - { - delete this.components[component] - } - - load() - { - const configuration = this.locate('core/configuration') - - // extending the configurations of every component - for(const component in this.components) - { - const config = this.fetchComponentConfig(component, this.components[component]) - configuration.extend(config) - } - - configuration.freeze() - - const - serviceMapUncomposed = configuration.find('core.locator'), - serviceMap = this.composeServiceMap(serviceMapUncomposed) - - // eager loading the services in the sevice locator - this.loadServiceRecursion(serviceMap) - } - - /** - * The ability to include by asterix is solved by this method - */ - composeServiceMap(serviceMapUncomposed) - { - const serviceMap = {} - - for(const serviceNameUncomposed in serviceMapUncomposed) - { - if(serviceNameUncomposed.endsWith('*')) - { - const - directoryPath = serviceMapUncomposed[serviceNameUncomposed].slice(0, -1), - dirents = fs.readdirSync(directoryPath, { withFileTypes:true }) - - for(const dirent of dirents) - { - if(dirent.isDirectory()) - { - const serviceNamePath = serviceNameUncomposed.slice(0, -1) - serviceMap[serviceNamePath + dirent.name] = directoryPath + dirent.name - } - } - } - else - { - serviceMap[serviceNameUncomposed] = serviceMapUncomposed[serviceNameUncomposed] - } - } - - return serviceMap - } - - fetchComponentConfig(component, pathname) - { - const - path = this.locate('core/path'), - specifiedPath = `${pathname}/config`, - localPath = `${path.main.dirname}/${component}/config`, - absolutePath = `${component}/config`, - corePath = `${__dirname}/${component}/config` - - if(path.isResolvable(specifiedPath)) - return require(specifiedPath) - - else if(path.isResolvable(localPath)) - return require(localPath) - - else if(path.isResolvable(absolutePath)) - return require(absolutePath) - - else if(path.isResolvable(corePath)) - return require(corePath) - - else - { - const - msg = `could not resolve path to component "${component}"`, - error = new Error(msg) - - error.code = 'E_COMPONENT_NOT_RESOLVABLE' - throw error - } - } - - loadService(serviceName, servicePath) - { - const - path = this.locator.locate('core/path'), - locatorPath = `${servicePath}/locator` - - if(path.isResolvable(locatorPath)) - { - let locator - - try - { - const Locator = require(locatorPath) - locator = new Locator(this.locator) - } - catch(exception) - { - const - msg = `Problem on initiation of the locator: "${locatorPath}" with the error message: "${exception.message}"`, - error = new Error(msg) - - throw error - } - - try - { - const service = locator.locate() - this.locator.set(serviceName, service) - } - catch(error) - { - switch(error.code) - { - case 'E_SERVICE_UNDEFINED': - { - const - msg = `An unmet dependency was found for service "${serviceName}", error: ${error.message}`, - errorUnmetDependency = new Error(msg) - - errorUnmetDependency.code = 'E_SERVICE_UNMET_DEPENDENCY' - throw errorUnmetDependency - } - - default: - throw error - } - } - } - else - { - const - msg = `locator could not be found for ${serviceName}`, - error = new Error(msg) - - error.code = 'E_SERVICE_LOCATOR_NOT_FOUND' - throw error - } - } - - /** - * Eager loading the services in the sevice locator. - * Recursion queue to complete loading all services. - * @param {Object} serviceMap [names of services] => [filepath of services] - */ - loadServiceRecursion(serviceMap) - { - const keys = Object.keys(serviceMap) - - // when the queue is empty, then we are done - if(keys.length === 0) - return - - // incomplete services that could not be loaded in the declared order - const queue = {} - - // looping through different service names in an attempt to eager load them - // if an "unmet dependency" error is thrown, the service name is pushed to a queue to be located at a later stage - // in hope that the earlier unmet dependency then is locatable - for(const serviceName in serviceMap) - { - try - { - this.loadService(serviceName, serviceMap[serviceName]) - } - catch(error) - { - switch (error.code) - { - case 'E_SERVICE_UNMET_DEPENDENCY': - queue[serviceName] = serviceMap[serviceName] - break; - - default: - throw error - } - } - } - - const queueKeys = Object.keys(queue) - - // if the new queue is the same as the old queue, then no progress has taken place - if(keys.length === queueKeys.length) - { - const error = new Error(`Unmet dependencies found, could not resolve dependencies for ${queueKeys.join(', ')}`) - error.code = 'E_SERVICE_UNABLE_TO_RESOLVE_DEPENDENCIES' - - throw error - } - - // recursion until the queue is empty - this.loadServiceRecursion(queue) - } - - locate(service) - { - return this.locator.locate(service) - } -} - -module.exports = Core +const fs = require('fs') + +class Core +{ + constructor(locator) + { + this.locator = locator + this.components = {} + } + + add(component, pathname) + { + this.components[component] = pathname + } + + remove(component) + { + delete this.components[component] + } + + load() + { + const configuration = this.locate('core/configuration') + + // extending the configurations of every component + for(const component in this.components) + { + const config = this.fetchComponentConfig(component, this.components[component]) + configuration.extend(config) + } + + configuration.freeze() + + const + serviceMapUncomposed = configuration.find('core.locator'), + serviceMap = this.composeServiceMap(serviceMapUncomposed) + + // eager loading the services in the sevice locator + this.loadServiceRecursion(serviceMap) + } + + /** + * The ability to include by asterix is solved by this method + */ + composeServiceMap(serviceMapUncomposed) + { + const serviceMap = {} + + for(const serviceNameUncomposed in serviceMapUncomposed) + { + if(serviceNameUncomposed.endsWith('*')) + { + const + directoryPath = serviceMapUncomposed[serviceNameUncomposed].slice(0, -1), + dirents = fs.readdirSync(directoryPath, { withFileTypes:true }) + + for(const dirent of dirents) + { + if(dirent.isDirectory()) + { + const serviceNamePath = serviceNameUncomposed.slice(0, -1) + serviceMap[serviceNamePath + dirent.name] = directoryPath + dirent.name + } + } + } + else + { + serviceMap[serviceNameUncomposed] = serviceMapUncomposed[serviceNameUncomposed] + } + } + + return serviceMap + } + + fetchComponentConfig(component, pathname) + { + const + path = this.locate('core/path'), + specifiedPath = `${pathname}/config`, + localPath = `${path.main.dirname}/${component}/config`, + absolutePath = `${component}/config`, + corePath = `${__dirname}/${component}/config` + + if(path.isResolvable(specifiedPath)) + return require(specifiedPath) + + else if(path.isResolvable(localPath)) + return require(localPath) + + else if(path.isResolvable(absolutePath)) + return require(absolutePath) + + else if(path.isResolvable(corePath)) + return require(corePath) + + else + { + const + msg = `could not resolve path to component "${component}"`, + error = new Error(msg) + + error.code = 'E_COMPONENT_NOT_RESOLVABLE' + throw error + } + } + + loadService(serviceName, servicePath) + { + const + path = this.locator.locate('core/path'), + locatorPath = `${servicePath}/locator` + + if(path.isResolvable(locatorPath)) + { + let locator + + try + { + const Locator = require(locatorPath) + locator = new Locator(this.locator) + } + catch(exception) + { + const + msg = `Problem on initiation of the locator: "${locatorPath}" with the error message: "${exception.message}"`, + error = new Error(msg) + + throw error + } + + try + { + const service = locator.locate() + this.locator.set(serviceName, service) + } + catch(error) + { + switch(error.code) + { + case 'E_SERVICE_UNDEFINED': + { + const + msg = `An unmet dependency was found for service "${serviceName}", error: ${error.message}`, + errorUnmetDependency = new Error(msg) + + errorUnmetDependency.code = 'E_SERVICE_UNMET_DEPENDENCY' + throw errorUnmetDependency + } + + default: + throw error + } + } + } + else + { + const + msg = `locator could not be found for ${serviceName}`, + error = new Error(msg) + + error.code = 'E_SERVICE_LOCATOR_NOT_FOUND' + throw error + } + } + + /** + * Eager loading the services in the sevice locator. + * Recursion queue to complete loading all services. + * @param {Object} serviceMap [names of services] => [filepath of services] + */ + loadServiceRecursion(serviceMap) + { + const keys = Object.keys(serviceMap) + + // when the queue is empty, then we are done + if(keys.length === 0) + return + + // incomplete services that could not be loaded in the declared order + const queue = {} + + // looping through different service names in an attempt to eager load them + // if an "unmet dependency" error is thrown, the service name is pushed to a queue to be located at a later stage + // in hope that the earlier unmet dependency then is locatable + for(const serviceName in serviceMap) + { + try + { + this.loadService(serviceName, serviceMap[serviceName]) + } + catch(error) + { + switch (error.code) + { + case 'E_SERVICE_UNMET_DEPENDENCY': + queue[serviceName] = serviceMap[serviceName] + break; + + default: + throw error + } + } + } + + const queueKeys = Object.keys(queue) + + // if the new queue is the same as the old queue, then no progress has taken place + if(keys.length === queueKeys.length) + { + const error = new Error(`Unmet dependencies found, could not resolve dependencies for ${queueKeys.join(', ')}`) + error.code = 'E_SERVICE_UNABLE_TO_RESOLVE_DEPENDENCIES' + + throw error + } + + // recursion until the queue is empty + this.loadServiceRecursion(queue) + } + + locate(service) + { + return this.locator.locate(service) + } +} + +module.exports = Core diff --git a/package.json b/package.json index a0c1796..1165b3c 100644 --- a/package.json +++ b/package.json @@ -1,58 +1,58 @@ -{ - "name": "superhero", - "version": "2.0.0-beta.82", - "description": "Framework built with the domain driven design pattern in mind", - "repository": "git@github.com:superhero/js.core.git", - "main": "index.js", - "license": "MIT", - "keywords": [ - "framework", - "http", - "server", - "eventbus", - "ddd" - ], - "author": { - "name": "Erik Landvall", - "email": "erik@landvall.se", - "url": "http://erik.landvall.se" - }, - "scripts": { - "superhero": "node ./bin/superhero.js", - "docs-coverage": "nyc mocha && nyc report --reporter=html --report-dir=./docs/generated/coverage", - "docs-tests": "mocha --reporter mochawesome --reporter-options reportDir=docs/generated/test,reportFilename=index,showHooks=always", - "test-coverage": "syntax-check && nyc mocha './**/test.*.js'", - "test": "mocha" - }, - "bin": { - "superhero": "./bin/superhero.js" - }, - "dependencies": { - "@superhero/request": "2.1.7", - "@superhero/debug": "1.1.13", - "cookies": "0.7.2" - }, - "devDependencies": { - "@superhero/syntax-check": "0.0.1", - "mocha": "6.2.1", - "mochawesome": "4.1.0", - "mocha-steps": "1.3.0", - "chai": "4.2.0", - "nyc": "14.1.1" - }, - "engines": { - "node": ">=10.0.0" - }, - "mocha": { - "require": [ - "test/init.js", - "mocha-steps" - ], - "ui": "bdd", - "full-trace": true, - "timeout": 5000, - "spec": [ - "./test/**/*.test.js" - ] - } -} +{ + "name": "superhero", + "version": "2.0.0-beta.82", + "description": "Framework built with the domain driven design pattern in mind", + "repository": "git@github.com:superhero/js.core.git", + "main": "index.js", + "license": "MIT", + "keywords": [ + "framework", + "http", + "server", + "eventbus", + "ddd" + ], + "author": { + "name": "Erik Landvall", + "email": "erik@landvall.se", + "url": "http://erik.landvall.se" + }, + "scripts": { + "superhero": "node ./bin/superhero.js", + "docs-coverage": "nyc mocha && nyc report --reporter=html --report-dir=./docs/generated/coverage", + "docs-tests": "mocha --reporter mochawesome --reporter-options reportDir=docs/generated/test,reportFilename=index,showHooks=always", + "test-coverage": "syntax-check && nyc mocha './**/test.*.js'", + "test": "mocha" + }, + "bin": { + "superhero": "./bin/superhero.js" + }, + "dependencies": { + "@superhero/request": "2.1.7", + "@superhero/debug": "1.1.13", + "cookies": "0.7.2" + }, + "devDependencies": { + "@superhero/syntax-check": "0.0.1", + "mocha": "6.2.1", + "mochawesome": "4.1.0", + "mocha-steps": "1.3.0", + "chai": "4.2.0", + "nyc": "14.1.1" + }, + "engines": { + "node": ">=10.0.0" + }, + "mocha": { + "require": [ + "test/init.js", + "mocha-steps" + ], + "ui": "bdd", + "full-trace": true, + "timeout": 5000, + "spec": [ + "./test/**/*.test.js" + ] + } +} diff --git a/test/api/config.js b/test/api/config.js index f5a9720..9a82dd7 100644 --- a/test/api/config.js +++ b/test/api/config.js @@ -1,75 +1,75 @@ -/** - * @namespace Api - */ -module.exports = -{ - core: - { - http: - { - server: - { - routes: - { - 'create-calculation': - { - url : '/calculations', - method : 'post', - endpoint: 'api/endpoint/create-calculation', - view : 'core/http/server/view/json', - input : false - }, - 'header-test': - { - url : '/header-test', - method : 'post', - endpoint: 'api/endpoint/header-test-endpoint', - view : 'core/http/server/view/json', - input : 'entity/header-test', - headers : - { - 'x-foo' : '.+', - 'x-bar' : 'bar' - } - }, - 'dto-test': - { - url : '/dto-test', - method : 'post', - endpoint: 'api/endpoint/dto-test-endpoint', - view : 'core/http/server/view/json', - input : 'entity/dto-test' - }, - 'authentication': - { - middleware : - [ - 'api/middleware/authentication' - ] - }, - 'append-calculation': - { - url : '/calculations/:id', - method : 'put', - endpoint: 'api/endpoint/append-calculation', - view : 'core/http/server/view/json', - input : 'entity/calculation' - } - } - } - }, - eventbus: - { - observers: - { - 'calculation created' : { 'api/observer/calculation-created/log' : true }, - 'calculation appended' : { 'api/observer/calculation-appended/log' : true } - } - }, - locator: - { - 'api/observer/calculation-created/log' : __dirname + '/observer/calculation-created/log', - 'api/observer/calculation-appended/log' : __dirname + '/observer/calculation-appended/log' - } - } -} +/** + * @namespace Api + */ +module.exports = +{ + core: + { + http: + { + server: + { + routes: + { + 'create-calculation': + { + url : '/calculations', + method : 'post', + endpoint: 'api/endpoint/create-calculation', + view : 'core/http/server/view/json', + input : false + }, + 'header-test': + { + url : '/header-test', + method : 'post', + endpoint: 'api/endpoint/header-test-endpoint', + view : 'core/http/server/view/json', + input : 'entity/header-test', + headers : + { + 'x-foo' : '.+', + 'x-bar' : 'bar' + } + }, + 'dto-test': + { + url : '/dto-test', + method : 'post', + endpoint: 'api/endpoint/dto-test-endpoint', + view : 'core/http/server/view/json', + input : 'entity/dto-test' + }, + 'authentication': + { + middleware : + [ + 'api/middleware/authentication' + ] + }, + 'append-calculation': + { + url : '/calculations/:id', + method : 'put', + endpoint: 'api/endpoint/append-calculation', + view : 'core/http/server/view/json', + input : 'entity/calculation' + } + } + } + }, + eventbus: + { + observers: + { + 'calculation created' : { 'api/observer/calculation-created/log' : true }, + 'calculation appended' : { 'api/observer/calculation-appended/log' : true } + } + }, + locator: + { + 'api/observer/calculation-created/log' : __dirname + '/observer/calculation-created/log', + 'api/observer/calculation-appended/log' : __dirname + '/observer/calculation-appended/log' + } + } +} diff --git a/test/api/endpoint/append-calculation.js b/test/api/endpoint/append-calculation.js index 2f9fe2a..ed5a894 100644 --- a/test/api/endpoint/append-calculation.js +++ b/test/api/endpoint/append-calculation.js @@ -1,38 +1,38 @@ -const -Dispatcher = require('../../../core/http/server/dispatcher'), -NotFoundError = require('../../../core/http/server/dispatcher/error/not-found'), -BadRequestError = require('../../../core/http/server/dispatcher/error/bad-request') - -/** - * @memberof Api - * @extends {superhero/core/http/server/dispatcher} - */ -class AppendCalculationEndpoint extends Dispatcher -{ - dispatch() - { - const - calculator = this.locator.locate('domain/aggregate/calculator'), - calculation = this.route.dto, - result = calculator.appendToCalculation(calculation) - - this.view.body.result = result - } - - onError(error) - { - switch(error) - { - case 'E_CALCULATION_COULD_NOT_BE_FOUND': - throw new NotFoundError('Calculation could not be found') - - case 'E_INVALID_CALCULATION_TYPE': - throw new BadRequestError(`Unrecognized type: "${this.route.dto.type}"`) - - default: - throw error - } - } -} - -module.exports = AppendCalculationEndpoint +const +Dispatcher = require('../../../core/http/server/dispatcher'), +NotFoundError = require('../../../core/http/server/dispatcher/error/not-found'), +BadRequestError = require('../../../core/http/server/dispatcher/error/bad-request') + +/** + * @memberof Api + * @extends {superhero/core/http/server/dispatcher} + */ +class AppendCalculationEndpoint extends Dispatcher +{ + dispatch() + { + const + calculator = this.locator.locate('domain/aggregate/calculator'), + calculation = this.route.dto, + result = calculator.appendToCalculation(calculation) + + this.view.body.result = result + } + + onError(error) + { + switch(error) + { + case 'E_CALCULATION_COULD_NOT_BE_FOUND': + throw new NotFoundError('Calculation could not be found') + + case 'E_INVALID_CALCULATION_TYPE': + throw new BadRequestError(`Unrecognized type: "${this.route.dto.type}"`) + + default: + throw error + } + } +} + +module.exports = AppendCalculationEndpoint diff --git a/test/api/endpoint/create-calculation.js b/test/api/endpoint/create-calculation.js index f5fc15b..f7016ac 100644 --- a/test/api/endpoint/create-calculation.js +++ b/test/api/endpoint/create-calculation.js @@ -1,19 +1,19 @@ -const Dispatcher = require('../../../core/http/server/dispatcher') - -/** - * @memberof Api - * @extends {superhero/core/http/server/dispatcher} - */ -class CreateCalculationEndpoint extends Dispatcher -{ - dispatch() - { - const - calculator = this.locator.locate('domain/aggregate/calculator'), - calculationId = calculator.createCalculation() - - this.view.body.id = calculationId - } -} - -module.exports = CreateCalculationEndpoint +const Dispatcher = require('../../../core/http/server/dispatcher') + +/** + * @memberof Api + * @extends {superhero/core/http/server/dispatcher} + */ +class CreateCalculationEndpoint extends Dispatcher +{ + dispatch() + { + const + calculator = this.locator.locate('domain/aggregate/calculator'), + calculationId = calculator.createCalculation() + + this.view.body.id = calculationId + } +} + +module.exports = CreateCalculationEndpoint diff --git a/test/api/endpoint/dto-test-endpoint.js b/test/api/endpoint/dto-test-endpoint.js index 674321f..2de1023 100644 --- a/test/api/endpoint/dto-test-endpoint.js +++ b/test/api/endpoint/dto-test-endpoint.js @@ -1,15 +1,15 @@ -const Dispatcher = require('../../../core/http/server/dispatcher') - -/** - * @memberof Api - * @extends {superhero/core/http/server/dispatcher} - */ -class DtoTestEndpoint extends Dispatcher -{ - dispatch() - { - this.view.meta.status = 200 - } -} - -module.exports = DtoTestEndpoint +const Dispatcher = require('../../../core/http/server/dispatcher') + +/** + * @memberof Api + * @extends {superhero/core/http/server/dispatcher} + */ +class DtoTestEndpoint extends Dispatcher +{ + dispatch() + { + this.view.meta.status = 200 + } +} + +module.exports = DtoTestEndpoint diff --git a/test/api/endpoint/header-test-endpoint.js b/test/api/endpoint/header-test-endpoint.js index 6e07855..c5b927c 100644 --- a/test/api/endpoint/header-test-endpoint.js +++ b/test/api/endpoint/header-test-endpoint.js @@ -1,15 +1,15 @@ -const Dispatcher = require('../../../core/http/server/dispatcher') - -/** - * @memberof Api - * @extends {superhero/core/http/server/dispatcher} - */ -class HeaderTestEndpoint extends Dispatcher -{ - dispatch() - { - this.view.meta.status = 200 - } -} - -module.exports = HeaderTestEndpoint +const Dispatcher = require('../../../core/http/server/dispatcher') + +/** + * @memberof Api + * @extends {superhero/core/http/server/dispatcher} + */ +class HeaderTestEndpoint extends Dispatcher +{ + dispatch() + { + this.view.meta.status = 200 + } +} + +module.exports = HeaderTestEndpoint diff --git a/test/api/middleware/authentication.js b/test/api/middleware/authentication.js index 257aff3..08acbd4 100644 --- a/test/api/middleware/authentication.js +++ b/test/api/middleware/authentication.js @@ -1,28 +1,28 @@ -const -Dispatcher = require('../../../core/http/server/dispatcher'), -Unauthorized = require('../../../core/http/server/dispatcher/error/unauthorized') - -/** - * @memberof Api - * @extends {superhero/core/http/server/dispatcher} - */ -class AuthenticationMiddleware extends Dispatcher -{ - async dispatch(next) - { - const - configuration = this.locator.locate('core/configuration'), - apikey = this.request.headers['api-key'] - - if(apikey === 'ABC123456789') - { - await next() - } - else - { - throw new Unauthorized('You are not authorized to access the requested resource') - } - } -} - -module.exports = AuthenticationMiddleware +const +Dispatcher = require('../../../core/http/server/dispatcher'), +Unauthorized = require('../../../core/http/server/dispatcher/error/unauthorized') + +/** + * @memberof Api + * @extends {superhero/core/http/server/dispatcher} + */ +class AuthenticationMiddleware extends Dispatcher +{ + async dispatch(next) + { + const + configuration = this.locator.locate('core/configuration'), + apikey = this.request.headers['api-key'] + + if(apikey === 'ABC123456789') + { + await next() + } + else + { + throw new Unauthorized('You are not authorized to access the requested resource') + } + } +} + +module.exports = AuthenticationMiddleware diff --git a/test/api/observer/calculation-appended/log/index.js b/test/api/observer/calculation-appended/log/index.js index 456b5bd..bc65e10 100644 --- a/test/api/observer/calculation-appended/log/index.js +++ b/test/api/observer/calculation-appended/log/index.js @@ -1,20 +1,20 @@ -/** - * @memberof Api - * @implements {superhero/core/eventbus/observer} - */ -class ObserverCalculationAppendedLog -{ - constructor(console, eventbus) - { - this.console = console - this.eventbus = eventbus - } - - observe(data) - { - this.console.log(data) - this.eventbus.emit('logged calculation appended event', data) - } -} - -module.exports = ObserverCalculationAppendedLog +/** + * @memberof Api + * @implements {superhero/core/eventbus/observer} + */ +class ObserverCalculationAppendedLog +{ + constructor(console, eventbus) + { + this.console = console + this.eventbus = eventbus + } + + observe(data) + { + this.console.log(data) + this.eventbus.emit('logged calculation appended event', data) + } +} + +module.exports = ObserverCalculationAppendedLog diff --git a/test/api/observer/calculation-appended/log/locator.js b/test/api/observer/calculation-appended/log/locator.js index 044e4cb..a7bda18 100644 --- a/test/api/observer/calculation-appended/log/locator.js +++ b/test/api/observer/calculation-appended/log/locator.js @@ -1,24 +1,24 @@ -const -ObserverCalculationAppendedLog = require('.'), -LocatorConstituent = require('../../../../../core/locator/constituent') - -/** - * @memberof Api - * @extends {superhero/core/locator/constituent} - */ -class ObserverCalculationAppendedLogLocator extends LocatorConstituent -{ - /** - * @returns {ObserverCalculationAppendedLog} - */ - locate() - { - const - console = this.locator.locate('core/console'), - eventbus = this.locator.locate('core/eventbus') - - return new ObserverCalculationAppendedLog(console, eventbus) - } -} - -module.exports = ObserverCalculationAppendedLogLocator +const +ObserverCalculationAppendedLog = require('.'), +LocatorConstituent = require('../../../../../core/locator/constituent') + +/** + * @memberof Api + * @extends {superhero/core/locator/constituent} + */ +class ObserverCalculationAppendedLogLocator extends LocatorConstituent +{ + /** + * @returns {ObserverCalculationAppendedLog} + */ + locate() + { + const + console = this.locator.locate('core/console'), + eventbus = this.locator.locate('core/eventbus') + + return new ObserverCalculationAppendedLog(console, eventbus) + } +} + +module.exports = ObserverCalculationAppendedLogLocator diff --git a/test/api/observer/calculation-created/log/index.js b/test/api/observer/calculation-created/log/index.js index e936b5e..7d91ac1 100644 --- a/test/api/observer/calculation-created/log/index.js +++ b/test/api/observer/calculation-created/log/index.js @@ -1,20 +1,20 @@ -/** - * @memberof Api - * @implements {superhero/core/eventbus/observer} - */ -class ObserverCalculationCreatedLog -{ - constructor(console, eventbus) - { - this.console = console - this.eventbus = eventbus - } - - observe(data) - { - this.console.log(data) - this.eventbus.emit('logged calculation created event', data) - } -} - -module.exports = ObserverCalculationCreatedLog +/** + * @memberof Api + * @implements {superhero/core/eventbus/observer} + */ +class ObserverCalculationCreatedLog +{ + constructor(console, eventbus) + { + this.console = console + this.eventbus = eventbus + } + + observe(data) + { + this.console.log(data) + this.eventbus.emit('logged calculation created event', data) + } +} + +module.exports = ObserverCalculationCreatedLog diff --git a/test/api/observer/calculation-created/log/locator.js b/test/api/observer/calculation-created/log/locator.js index 65b8255..974515f 100644 --- a/test/api/observer/calculation-created/log/locator.js +++ b/test/api/observer/calculation-created/log/locator.js @@ -1,24 +1,24 @@ -const -ObserverCalculationCreatedLog = require('.'), -LocatorConstituent = require('../../../../../core/locator/constituent') - -/** - * @memberof Api - * @extends {superhero/core/locator/constituent} - */ -class ObserverCalculationCreatedLogLocator extends LocatorConstituent -{ - /** - * @returns {ObserverCalculationCreatedLog} - */ - locate() - { - const - console = this.locator.locate('core/console'), - eventbus = this.locator.locate('core/eventbus') - - return new ObserverCalculationCreatedLog(console, eventbus) - } -} - -module.exports = ObserverCalculationCreatedLogLocator +const +ObserverCalculationCreatedLog = require('.'), +LocatorConstituent = require('../../../../../core/locator/constituent') + +/** + * @memberof Api + * @extends {superhero/core/locator/constituent} + */ +class ObserverCalculationCreatedLogLocator extends LocatorConstituent +{ + /** + * @returns {ObserverCalculationCreatedLog} + */ + locate() + { + const + console = this.locator.locate('core/console'), + eventbus = this.locator.locate('core/eventbus') + + return new ObserverCalculationCreatedLog(console, eventbus) + } +} + +module.exports = ObserverCalculationCreatedLogLocator diff --git a/test/bad-request.test.js b/test/bad-request.test.js index a7bb577..84c552a 100644 --- a/test/bad-request.test.js +++ b/test/bad-request.test.js @@ -1,61 +1,61 @@ -describe('Bad request test', () => -{ - const - expect = require('chai').expect, - context = require('mochawesome/addContext') - - let core - - before((done) => - { - const - CoreFactory = require('../core/factory'), - coreFactory = new CoreFactory - - core = coreFactory.create() - - core.add('api') - core.add('domain') - - core.load() - - core.locate('core/bootstrap').bootstrap().then(() => - { - core.locate('core/http/server').listen(9002) - core.locate('core/http/server').onListening(done) - }) - }) - - after(() => - { - core.locate('core/http/server').close() - }) - - it('should return a 400 status bad request when the request body does not match the expected input', async function() - { - const configuration = core.locate('core/configuration') - const httpRequest = core.locate('core/http/request') - context(this, { title:'route', value:configuration.find('core.http.server.routes.dto-test') }) - const url = 'http://localhost:9002/dto-test' - const data = { foo:'bar' } - const response = await httpRequest.post({ url, data }) - expect(response.status).to.be.equal(400) - }) - - it('should return a 404 if the header does not match the expected value', async function() - { - const configuration = core.locate('core/configuration') - const httpRequest = core.locate('core/http/request') - context(this, { title:'route', value:configuration.find('core.http.server.routes.dto-test') }) - const url = 'http://localhost:9002/header-test' - - const response1 = await httpRequest.post({ url, headers:{ 'x-foo':'foo' } }) - expect(response1.status).to.be.equal(404) - - const response2 = await httpRequest.post({ url, headers:{ 'x-foo':'bar', 'x-bar':'foo' } }) - expect(response2.status).to.be.equal(404) - - const response3 = await httpRequest.post({ url, headers:{ 'x-foo':'foo', 'x-bar':'bar' } }) - expect(response3.status).to.be.equal(200) - }) -}) +describe('Bad request test', () => +{ + const + expect = require('chai').expect, + context = require('mochawesome/addContext') + + let core + + before((done) => + { + const + CoreFactory = require('../core/factory'), + coreFactory = new CoreFactory + + core = coreFactory.create() + + core.add('api') + core.add('domain') + + core.load() + + core.locate('core/bootstrap').bootstrap().then(() => + { + core.locate('core/http/server').listen(9002) + core.locate('core/http/server').onListening(done) + }) + }) + + after(() => + { + core.locate('core/http/server').close() + }) + + it('should return a 400 status bad request when the request body does not match the expected input', async function() + { + const configuration = core.locate('core/configuration') + const httpRequest = core.locate('core/http/request') + context(this, { title:'route', value:configuration.find('core.http.server.routes.dto-test') }) + const url = 'http://localhost:9002/dto-test' + const data = { foo:'bar' } + const response = await httpRequest.post({ url, data }) + expect(response.status).to.be.equal(400) + }) + + it('should return a 404 if the header does not match the expected value', async function() + { + const configuration = core.locate('core/configuration') + const httpRequest = core.locate('core/http/request') + context(this, { title:'route', value:configuration.find('core.http.server.routes.dto-test') }) + const url = 'http://localhost:9002/header-test' + + const response1 = await httpRequest.post({ url, headers:{ 'x-foo':'foo' } }) + expect(response1.status).to.be.equal(404) + + const response2 = await httpRequest.post({ url, headers:{ 'x-foo':'bar', 'x-bar':'foo' } }) + expect(response2.status).to.be.equal(404) + + const response3 = await httpRequest.post({ url, headers:{ 'x-foo':'foo', 'x-bar':'bar' } }) + expect(response3.status).to.be.equal(200) + }) +}) diff --git a/test/calculation.test.js b/test/calculation.test.js index b75d89f..69535bf 100644 --- a/test/calculation.test.js +++ b/test/calculation.test.js @@ -1,57 +1,57 @@ -describe('Calculations', () => -{ - const - expect = require('chai').expect, - context = require('mochawesome/addContext') - - let core - - before((done) => - { - const - CoreFactory = require('../core/factory'), - coreFactory = new CoreFactory - - core = coreFactory.create() - - core.add('api') - core.add('domain') - - core.load() - - core.locate('core/bootstrap').bootstrap().then(() => - { - core.locate('core/http/server').listen(9002) - core.locate('core/http/server').onListening(done) - }) - }) - - after(() => - { - core.locate('core/http/server').close() - }) - - it('A client can create a calculation', async function() - { - const configuration = core.locate('core/configuration') - const httpRequest = core.locate('core/http/request') - context(this, { title:'route', value:configuration.find('core.http.server.routes.create-calculation') }) - const response = await httpRequest.post('http://localhost:9002/calculations') - expect(response.data.id).to.be.equal(1) - }) - - it('A client can append a calculation to the result of a former calculation if authentication Api-Key', async function() - { - const configuration = core.locate('core/configuration') - const httpRequest = core.locate('core/http/request') - context(this, { title:'route', value:configuration.find('core.http.server.routes.append-calculation') }) - const url = 'http://localhost:9002/calculations/1' - const data = { id:1, type:'addition', value:100 } - const response_unauthorized = await httpRequest.put({ url, data }) - // console.log('* * * * * * * * * *', response_unauthorized.data) - expect(response_unauthorized.status).to.be.equal(401) - const headers = { 'api-key':'ABC123456789' } - const response_authorized = await httpRequest.put({ headers, url, data }) - expect(response_authorized.data.result).to.be.equal(data.value) - }) -}) +describe('Calculations', () => +{ + const + expect = require('chai').expect, + context = require('mochawesome/addContext') + + let core + + before((done) => + { + const + CoreFactory = require('../core/factory'), + coreFactory = new CoreFactory + + core = coreFactory.create() + + core.add('api') + core.add('domain') + + core.load() + + core.locate('core/bootstrap').bootstrap().then(() => + { + core.locate('core/http/server').listen(9002) + core.locate('core/http/server').onListening(done) + }) + }) + + after(() => + { + core.locate('core/http/server').close() + }) + + it('A client can create a calculation', async function() + { + const configuration = core.locate('core/configuration') + const httpRequest = core.locate('core/http/request') + context(this, { title:'route', value:configuration.find('core.http.server.routes.create-calculation') }) + const response = await httpRequest.post('http://localhost:9002/calculations') + expect(response.data.id).to.be.equal(1) + }) + + it('A client can append a calculation to the result of a former calculation if authentication Api-Key', async function() + { + const configuration = core.locate('core/configuration') + const httpRequest = core.locate('core/http/request') + context(this, { title:'route', value:configuration.find('core.http.server.routes.append-calculation') }) + const url = 'http://localhost:9002/calculations/1' + const data = { id:1, type:'addition', value:100 } + const response_unauthorized = await httpRequest.put({ url, data }) + // console.log('* * * * * * * * * *', response_unauthorized.data) + expect(response_unauthorized.status).to.be.equal(401) + const headers = { 'api-key':'ABC123456789' } + const response_authorized = await httpRequest.put({ headers, url, data }) + expect(response_authorized.data.result).to.be.equal(data.value) + }) +}) diff --git a/test/composer.test.js b/test/composer.test.js index d0d2112..dc2ea03 100644 --- a/test/composer.test.js +++ b/test/composer.test.js @@ -1,33 +1,33 @@ -describe('Composer', () => -{ - const - expect = require('chai').expect, - context = require('mochawesome/addContext') - - let core - - before((done) => - { - const - CoreFactory = require('../core/factory'), - coreFactory = new CoreFactory - - core = coreFactory.create() - - core.add('api') - core.add('domain') - - core.load() - - core.locate('core/bootstrap').bootstrap().then(done) - }) - - it('should be able to create an example', function() - { - const - composer = core.locate('core/schema/composer'), - example = composer.composeExample('entity/calculation') - - expect(example).to.deep.equal({ id: 123, type: 'addition', value: 123.45 }) - }) -}) +describe('Composer', () => +{ + const + expect = require('chai').expect, + context = require('mochawesome/addContext') + + let core + + before((done) => + { + const + CoreFactory = require('../core/factory'), + coreFactory = new CoreFactory + + core = coreFactory.create() + + core.add('api') + core.add('domain') + + core.load() + + core.locate('core/bootstrap').bootstrap().then(done) + }) + + it('should be able to create an example', function() + { + const + composer = core.locate('core/schema/composer'), + example = composer.composeExample('entity/calculation') + + expect(example).to.deep.equal({ id: 123, type: 'addition', value: 123.45 }) + }) +}) diff --git a/test/domain/aggregate/calculator/error/calculation-could-not-be-found.js b/test/domain/aggregate/calculator/error/calculation-could-not-be-found.js index 6a16f45..313b6db 100644 --- a/test/domain/aggregate/calculator/error/calculation-could-not-be-found.js +++ b/test/domain/aggregate/calculator/error/calculation-could-not-be-found.js @@ -1,14 +1,14 @@ -/** - * @memberof Domain - * @extends {Error} - */ -class CalculationCouldNotBeFoundError extends Error -{ - constructor(...args) - { - super(...args) - this.code = 'E_CALCULATION_COULD_NOT_BE_FOUND' - } -} - -module.exports = CalculationCouldNotBeFoundError +/** + * @memberof Domain + * @extends {Error} + */ +class CalculationCouldNotBeFoundError extends Error +{ + constructor(...args) + { + super(...args) + this.code = 'E_CALCULATION_COULD_NOT_BE_FOUND' + } +} + +module.exports = CalculationCouldNotBeFoundError diff --git a/test/domain/aggregate/calculator/error/invalid-calculation-type.js b/test/domain/aggregate/calculator/error/invalid-calculation-type.js index ea3b4a7..8cf6774 100644 --- a/test/domain/aggregate/calculator/error/invalid-calculation-type.js +++ b/test/domain/aggregate/calculator/error/invalid-calculation-type.js @@ -1,14 +1,14 @@ -/** - * @memberof Domain - * @extends {Error} - */ -class InvalidCalculationTypeError extends Error -{ - constructor(...args) - { - super(...args) - this.code = 'E_INVALID_CALCULATION_TYPE' - } -} - -module.exports = InvalidCalculationTypeError +/** + * @memberof Domain + * @extends {Error} + */ +class InvalidCalculationTypeError extends Error +{ + constructor(...args) + { + super(...args) + this.code = 'E_INVALID_CALCULATION_TYPE' + } +} + +module.exports = InvalidCalculationTypeError diff --git a/test/domain/aggregate/calculator/index.js b/test/domain/aggregate/calculator/index.js index 5a7522c..c9585f9 100644 --- a/test/domain/aggregate/calculator/index.js +++ b/test/domain/aggregate/calculator/index.js @@ -1,68 +1,68 @@ -const -CalculationCouldNotBeFoundError = require('./error/calculation-could-not-be-found'), -InvalidCalculationTypeError = require('./error/invalid-calculation-type') - -/** - * Calculator service, manages calculations - * @memberof Domain - */ -class AggregateCalculator -{ - /** - * @param {superhero/core/eventbus} eventbus - */ - constructor(eventbus) - { - this.eventbus = eventbus - this.calculations = [] - } - - /** - * @returns {number} the id of the created calculation - */ - createCalculation() - { - const id = this.calculations.push(0) - this.eventbus.emit('calculation created', { id }) - return id - } - - /** - * @throws {E_CALCULATION_COULD_NOT_BE_FOUND} - * @throws {E_INVALID_CALCULATION_TYPE} - * - * @param {CalculatorCalculation} dto - * - * @returns {number} the result of the calculation - */ - appendToCalculation({ id, type, value }) - { - if(id < 1 - || id > this.calculations.length) - { - throw new CalculationCouldNotBeFoundError(`ID out of range: "${id}/${this.calculations.length}"`) - } - - switch(type) - { - case 'addition': - { - const result = this.calculations[id - 1] += value - this.eventbus.emit('calculation appended', { id, type, result }) - return result - } - case 'subtraction': - { - const result = this.calculations[id - 1] -= value - this.eventbus.emit('calculation appended', { id, type, result }) - return result - } - default: - { - throw new InvalidCalculationTypeError(`Unrecognized type used for calculation: "${type}"`) - } - } - } -} - -module.exports = AggregateCalculator +const +CalculationCouldNotBeFoundError = require('./error/calculation-could-not-be-found'), +InvalidCalculationTypeError = require('./error/invalid-calculation-type') + +/** + * Calculator service, manages calculations + * @memberof Domain + */ +class AggregateCalculator +{ + /** + * @param {superhero/core/eventbus} eventbus + */ + constructor(eventbus) + { + this.eventbus = eventbus + this.calculations = [] + } + + /** + * @returns {number} the id of the created calculation + */ + createCalculation() + { + const id = this.calculations.push(0) + this.eventbus.emit('calculation created', { id }) + return id + } + + /** + * @throws {E_CALCULATION_COULD_NOT_BE_FOUND} + * @throws {E_INVALID_CALCULATION_TYPE} + * + * @param {CalculatorCalculation} dto + * + * @returns {number} the result of the calculation + */ + appendToCalculation({ id, type, value }) + { + if(id < 1 + || id > this.calculations.length) + { + throw new CalculationCouldNotBeFoundError(`ID out of range: "${id}/${this.calculations.length}"`) + } + + switch(type) + { + case 'addition': + { + const result = this.calculations[id - 1] += value + this.eventbus.emit('calculation appended', { id, type, result }) + return result + } + case 'subtraction': + { + const result = this.calculations[id - 1] -= value + this.eventbus.emit('calculation appended', { id, type, result }) + return result + } + default: + { + throw new InvalidCalculationTypeError(`Unrecognized type used for calculation: "${type}"`) + } + } + } +} + +module.exports = AggregateCalculator diff --git a/test/domain/aggregate/calculator/locator.js b/test/domain/aggregate/calculator/locator.js index 0dae70c..6622467 100644 --- a/test/domain/aggregate/calculator/locator.js +++ b/test/domain/aggregate/calculator/locator.js @@ -1,21 +1,21 @@ -const -AggregateCalculator = require('.'), -LocatorConstituent = require('../../../../core/locator/constituent') - -/** - * @memberof Domain - * @extends {superhero/core/locator/constituent} - */ -class AggregateCalculatorLocator extends LocatorConstituent -{ - /** - * @returns {AggregateCalculator} - */ - locate() - { - const eventbus = this.locator.locate('core/eventbus') - return new AggregateCalculator(eventbus) - } -} - -module.exports = AggregateCalculatorLocator +const +AggregateCalculator = require('.'), +LocatorConstituent = require('../../../../core/locator/constituent') + +/** + * @memberof Domain + * @extends {superhero/core/locator/constituent} + */ +class AggregateCalculatorLocator extends LocatorConstituent +{ + /** + * @returns {AggregateCalculator} + */ + locate() + { + const eventbus = this.locator.locate('core/eventbus') + return new AggregateCalculator(eventbus) + } +} + +module.exports = AggregateCalculatorLocator diff --git a/test/domain/config.js b/test/domain/config.js index d931212..73234b9 100644 --- a/test/domain/config.js +++ b/test/domain/config.js @@ -1,21 +1,21 @@ -/** - * @namespace Domain - */ -module.exports = -{ - core: - { - schema: - { - composer: - { - 'entity/*' : __dirname + '/schema/entity/*', - 'value-object/*' : __dirname + '/schema/value-object/*' - } - }, - locator: - { - 'domain/aggregate/*' : __dirname + '/aggregate/*' - } - } -} +/** + * @namespace Domain + */ +module.exports = +{ + core: + { + schema: + { + composer: + { + 'entity/*' : __dirname + '/schema/entity/*', + 'value-object/*' : __dirname + '/schema/value-object/*' + } + }, + locator: + { + 'domain/aggregate/*' : __dirname + '/aggregate/*' + } + } +} diff --git a/test/domain/schema/entity/calculation.js b/test/domain/schema/entity/calculation.js index c0f7fbe..e07bec5 100644 --- a/test/domain/schema/entity/calculation.js +++ b/test/domain/schema/entity/calculation.js @@ -1,33 +1,33 @@ -/** - * @memberof Domain - * @typedef {Object} EntityCalculation - * @property {number} id - * @property {string} type - * @property {number} value - */ -const entity = -{ - 'id': - { - 'type' : 'schema', - 'schema' : 'value-object/id', - 'trait' : 'id' - }, - 'type': - { - 'type': 'string', - 'enum': - [ - 'addition', - 'subtraction' - ], - 'example' : 'addition' - }, - 'value': - { - 'type' : 'decimal', - 'example' : 123.45 - } -} - -module.exports = entity +/** + * @memberof Domain + * @typedef {Object} EntityCalculation + * @property {number} id + * @property {string} type + * @property {number} value + */ +const entity = +{ + 'id': + { + 'type' : 'schema', + 'schema' : 'value-object/id', + 'trait' : 'id' + }, + 'type': + { + 'type': 'string', + 'enum': + [ + 'addition', + 'subtraction' + ], + 'example' : 'addition' + }, + 'value': + { + 'type' : 'decimal', + 'example' : 123.45 + } +} + +module.exports = entity diff --git a/test/domain/schema/entity/dto-test.js b/test/domain/schema/entity/dto-test.js index e031fef..ca5fdfa 100644 --- a/test/domain/schema/entity/dto-test.js +++ b/test/domain/schema/entity/dto-test.js @@ -1,21 +1,21 @@ -/** - * @memberof Domain - * @typedef {Object} EntityDtoTest - * @property {string} test - */ -const entity = -{ - 'test': - { - 'type': 'string', - 'enum': - [ - 'foo', - 'bar', - 'baz', - 'qux' - ] - } -} - -module.exports = entity +/** + * @memberof Domain + * @typedef {Object} EntityDtoTest + * @property {string} test + */ +const entity = +{ + 'test': + { + 'type': 'string', + 'enum': + [ + 'foo', + 'bar', + 'baz', + 'qux' + ] + } +} + +module.exports = entity diff --git a/test/domain/schema/entity/header-test.js b/test/domain/schema/entity/header-test.js index 926c21c..fefc5cb 100644 --- a/test/domain/schema/entity/header-test.js +++ b/test/domain/schema/entity/header-test.js @@ -1,9 +1,9 @@ -/** - * @memberof Domain - * @typedef {Object} EntityHeaderTest - */ -const entity = -{ -} - -module.exports = entity +/** + * @memberof Domain + * @typedef {Object} EntityHeaderTest + */ +const entity = +{ +} + +module.exports = entity diff --git a/test/domain/schema/value-object/id.js b/test/domain/schema/value-object/id.js index d8f3353..563a19f 100644 --- a/test/domain/schema/value-object/id.js +++ b/test/domain/schema/value-object/id.js @@ -1,16 +1,16 @@ -/** - * @memberof Domain - * @typedef {Object} ValueObjectId - * @property {number} id - */ -const valueObject = -{ - 'id': - { - 'type' : 'integer', - 'unsigned': true, - 'example' : 123 - } -} - -module.exports = valueObject +/** + * @memberof Domain + * @typedef {Object} ValueObjectId + * @property {number} id + */ +const valueObject = +{ + 'id': + { + 'type' : 'integer', + 'unsigned': true, + 'example' : 123 + } +} + +module.exports = valueObject diff --git a/test/init.js b/test/init.js index 06ac096..3696e70 100644 --- a/test/init.js +++ b/test/init.js @@ -1,2 +1,2 @@ -require.main.filename = __filename -require.main.dirname = __dirname +require.main.filename = __filename +require.main.dirname = __dirname diff --git a/test/logger.test.js b/test/logger.test.js index 487068a..0c82450 100644 --- a/test/logger.test.js +++ b/test/logger.test.js @@ -1,33 +1,33 @@ -describe('Logger', () => -{ - const - expect = require('chai').expect, - context = require('mochawesome/addContext') - - let core - - before((done) => - { - const - CoreFactory = require('../core/factory'), - coreFactory = new CoreFactory - - core = coreFactory.create() - - core.add('api') - core.add('domain') - - core.load() - - core.locate('core/bootstrap').bootstrap().then(done) - }) - - it('the logger is logging', function(done) - { - const configuration = core.locate('core/configuration') - const eventbus = core.locate('core/eventbus') - context(this, { title:'observers', value:configuration.find('core.http.eventbus.observers') }) - eventbus.once('logged calculation created event', () => done()) - eventbus.emit('calculation created', 'test') - }) -}) +describe('Logger', () => +{ + const + expect = require('chai').expect, + context = require('mochawesome/addContext') + + let core + + before((done) => + { + const + CoreFactory = require('../core/factory'), + coreFactory = new CoreFactory + + core = coreFactory.create() + + core.add('api') + core.add('domain') + + core.load() + + core.locate('core/bootstrap').bootstrap().then(done) + }) + + it('the logger is logging', function(done) + { + const configuration = core.locate('core/configuration') + const eventbus = core.locate('core/eventbus') + context(this, { title:'observers', value:configuration.find('core.http.eventbus.observers') }) + eventbus.once('logged calculation created event', () => done()) + eventbus.emit('calculation created', 'test') + }) +})