├── .editorconfig ├── .env.example ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .npmignore ├── .pr-bumper.json ├── .travis.yml ├── .travis └── deployment.key.pem.enc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── DEVELOPER.md ├── LICENSE ├── README.md ├── dependency-snapshot.json ├── docker └── common-services.yml ├── docs ├── .nojekyll ├── 404.md ├── CNAME ├── api.md ├── articles.md ├── articles │ ├── why-bother.md │ └── zeroth-for-the-angular-developer.md ├── assets │ ├── css │ │ └── main.css │ └── image │ │ ├── logo.svg │ │ ├── logotype.svg │ │ ├── title-card.jpg │ │ └── zeroth-cli.png ├── changelog.md ├── faq.md ├── guide.md ├── guide │ ├── application-lifecycle.md │ ├── cli.md │ ├── configuration.md │ ├── controllers.md │ ├── database.md │ ├── dependency-injection.md │ ├── deployment.md │ ├── documentation.md │ ├── email.md │ ├── exceptions.md │ ├── logging.md │ ├── middleware.md │ ├── migrations.md │ ├── model-stores.md │ ├── models.md │ ├── queues.md │ ├── quick-start.md │ ├── routing.md │ ├── seeding.md │ ├── services.md │ ├── testing.md │ └── validation.md ├── index.md └── templates │ └── home.hbs ├── package.json ├── src ├── browser │ ├── bootstrap.spec.ts │ ├── index.ts │ ├── polyfills.ts │ ├── stores │ │ ├── http.store.spec.ts │ │ ├── http.store.ts │ │ └── index.ts │ └── vendor.ts ├── common │ ├── exceptions │ │ ├── exceptions.spec.ts │ │ ├── exceptions.ts │ │ └── index.ts │ ├── index.ts │ ├── metadata │ │ └── metadata.ts │ ├── models │ │ ├── collection.spec.ts │ │ ├── collection.ts │ │ ├── index.ts │ │ ├── model.spec.ts │ │ ├── model.ts │ │ ├── relations │ │ │ ├── belongsTo.decorator.ts │ │ │ ├── hand.model.fixture.ts │ │ │ ├── hasOne.decorator.ts │ │ │ ├── index.ts │ │ │ ├── relations.spec.ts │ │ │ └── thumb.model.fixture.ts │ │ └── types │ │ │ ├── index.ts │ │ │ ├── primary.decorator.ts │ │ │ ├── storedProperty.decorator.ts │ │ │ └── timestamp.decorator.ts │ ├── registry │ │ ├── decorators.ts │ │ ├── entityRegistry.ts │ │ └── index.ts │ ├── services │ │ ├── consoleLogger.service.spec.ts │ │ ├── consoleLogger.service.ts │ │ ├── index.ts │ │ ├── logger.service.mock.ts │ │ ├── logger.service.spec.ts │ │ ├── logger.service.ts │ │ └── service.ts │ ├── stores │ │ ├── index.ts │ │ ├── mock.store.ts │ │ ├── store.spec.ts │ │ └── store.ts │ ├── util │ │ ├── banner.ts │ │ ├── serialPromise.spec.ts │ │ └── serialPromise.ts │ └── validation │ │ └── index.ts └── server │ ├── bootstrap.spec.ts │ ├── bootstrap │ ├── bootstrap.spec.ts │ ├── bootstrap.ts │ ├── controllers.bootstrapper.spec.ts │ ├── controllers.bootstrapper.ts │ ├── entity.bootstrapper.ts │ ├── index.ts │ ├── migrations.bootstrapper.spec.ts │ ├── migrations.bootstrapper.ts │ ├── models.bootstrapper.spec.ts │ ├── models.bootstrapper.ts │ ├── seeders.bootstrapper.spec.ts │ ├── seeders.bootstrapper.ts │ ├── services.bootstrapper.spec.ts │ └── services.bootstrapper.ts │ ├── controllers │ ├── abstract.controller.spec.ts │ ├── abstract.controller.ts │ ├── index.ts │ ├── request.spec.ts │ ├── request.ts │ ├── resource.controller.spec.ts │ ├── resource.controller.ts │ ├── response.spec.ts │ ├── response.ts │ ├── route.decorator.spec.ts │ └── route.decorator.ts │ ├── index.ts │ ├── main.ts │ ├── middleware │ ├── debugLog.middleware.spec.ts │ ├── debugLog.middleware.ts │ ├── index.ts │ ├── middleware.decorator.ts │ └── middleware.spec.ts │ ├── migrations │ └── index.ts │ ├── registry │ ├── decorators.ts │ ├── entityRegistry.spec.ts │ └── index.ts │ ├── seeders │ └── index.ts │ ├── servers │ ├── abstract.server.mock.ts │ ├── abstract.server.spec.ts │ ├── abstract.server.ts │ ├── express.server.spec.ts │ ├── express.server.ts │ ├── hapi.server.spec.ts │ ├── hapi.server.ts │ └── index.ts │ ├── services │ ├── auth.service.mock.ts │ ├── auth.service.spec.ts │ ├── auth.service.ts │ ├── database.service.mock.ts │ ├── database.service.spec.ts │ ├── database.service.ts │ ├── index.ts │ ├── jwtAuthStrategy.spec.ts │ ├── jwtAuthStrategy.ts │ ├── remoteCli.service.mock.ts │ ├── remoteCli.service.spec.ts │ ├── remoteCli.service.ts │ └── vantage.d.ts │ └── stores │ ├── db.store.spec.ts │ ├── db.store.ts │ └── index.ts ├── tsconfig.browser.json ├── tsconfig.json ├── tsconfig.server.json ├── tslint.json └── zeroth.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | max_line_length = 0 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | VERBOSITY=silly 2 | API_BASE=/api 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **I'm submitting a ...** 2 | - [ ] bug report 3 | - [ ] feature request 4 | - [ ] support request => Please do not submit support request here, post your question in the [gitter channel](https://gitter.im/zerothstack/zeroth/) 5 | 6 | **What features are related to this issue** 7 | - [ ] Quick Start 8 | - [ ] Application Lifecycle 9 | - [ ] CLI 10 | - [ ] Configuration 11 | - [ ] Controllers 12 | - [ ] Database 13 | - [ ] Dependency Injection 14 | - [ ] Deployment 15 | - [ ] Documentation 16 | - [ ] Email 17 | - [ ] Exceptions 18 | - [ ] Logging 19 | - [ ] Middleware 20 | - [ ] Migrations 21 | - [ ] Model Stores 22 | - [ ] Models 23 | - [ ] Queues 24 | - [ ] Routing 25 | - [ ] Seeding 26 | - [ ] Services 27 | - [ ] Testing 28 | - [ ] Validation 29 | 30 | 31 | **Do you want to request a *feature* or report a *bug*?** 32 | - [ ] Feature 33 | - [ ] Bug 34 | - [ ] Something else 35 | 36 | **What is the current behavior?** 37 | 38 | 39 | 40 | **If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem** by creating a github repo. 41 | 42 | 43 | 44 | **What is the expected behavior?** 45 | 46 | 47 | 48 | **What is the motivation / use case for changing the behavior?** 49 | 50 | 51 | 52 | **Please tell us about your environment:** 53 | 54 | - Zeroth version: 0.x.x 55 | - NodeJS version: x.x.x 56 | - Browser: [all | Chrome xx | Firefox xx | IE xx | Safari xx ] 57 | - OS: [all | Mac OS X | Windows | Linux ] 58 | 59 | 60 | **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow, gitter, etc) 61 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### This project uses [semver](semver.org), please check the scope of this pr: 2 | - [ ] #patch# - backwards-compatible bug fix 3 | - [ ] #minor# - adding functionality in a backwards-compatible manner 4 | - [ ] #major# - incompatible API change 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /dist-docs 6 | /lib 7 | /tmp 8 | 9 | # dependencies 10 | /node_modules 11 | 12 | # IDEs and editors 13 | /.idea 14 | 15 | # misc 16 | /.sass-cache 17 | /connect.lock 18 | /coverage/* 19 | /libpeerconnection.log 20 | npm-debug.log 21 | testem.log 22 | /typings 23 | 24 | # e2e 25 | /e2e/*.js 26 | /e2e/*.map 27 | 28 | #System Files 29 | .DS_Store 30 | 31 | #config 32 | .env 33 | !.npmignore 34 | 35 | #deployment keys 36 | .travis/*.pem 37 | 38 | # hoisted /lib files 39 | /index.* 40 | /browser 41 | /common 42 | /server 43 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | #ignore all 2 | /* 3 | 4 | !/docker/** 5 | !/common/** 6 | !/browser/** 7 | !/server/** 8 | !/index.* 9 | 10 | # root docs 11 | #!/README.md 12 | #!/CHANGELOG.md 13 | 14 | # root config 15 | !/.env.example 16 | -------------------------------------------------------------------------------- /.pr-bumper.json: -------------------------------------------------------------------------------- 1 | { 2 | "prependChangelog": false 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | language: node_js 4 | node_js: 5 | - 6.9.1 6 | addons: 7 | apt: 8 | sources: 9 | - ubuntu-toolchain-r-test 10 | - google-chrome 11 | packages: 12 | - g++-4.8 13 | - google-chrome-stable 14 | cache: 15 | directories: 16 | - node_modules 17 | branches: 18 | except: 19 | - "/^v[0-9\\.]+/" 20 | before_install: 21 | - npm install -g pr-bumper 22 | - pr-bumper check 23 | - cp .env.example .env 24 | - sudo apt-get update 25 | - export CHROME_BIN=/usr/bin/google-chrome 26 | - export DISPLAY=:99.0 27 | - sh -e /etc/init.d/xvfb start 28 | install: 29 | - travis_retry npm install 30 | script: 31 | - npm test 32 | - npm run build 33 | after_success: 34 | - npm run coveralls 35 | before_deploy: 36 | - eval "$(ssh-agent -s)" 37 | - openssl aes-256-cbc -K $encrypted_953c8d6bcb38_key -iv $encrypted_953c8d6bcb38_iv 38 | -in .travis/deployment.key.pem.enc -out .travis/deployment.key.pem -d 39 | - chmod 600 .travis/deployment.key.pem 40 | - ssh-add .travis/deployment.key.pem 41 | - pr-bumper bump 42 | - git fetch --tags 43 | - npm run preparepublish 44 | env: 45 | global: 46 | - secure: Ht4a2ay7PKrzH8A2WVnP6+foA+BZ7jUkK6Q3/GP0fHmxkvt7NT7Q2FN8yXGFQOk6qd6PIewbcUmhj0CG74AEyDb6F0qyKUZvv6f1YvMSDd+S0BYkPt3CkPdkpxVSaOQSPk84+JnDGXrB8roDviuw6zuqSIM6iKPCgAcLFyB1fxG0RUjpdA4MiNgL/e4IT8tncF4kR3z83etHQfgECF5CUF4rNXL0LDIIVeslW6mVzggKfao/0TvJsMsau60Cmk/CC46Vs8SUp1UwAFsRuazU/sNgGJaqeAIUelWjicbFViV8fIuUWfMvV3jM6sc3WsSeDrKixNZNvCJhbSefFX9MrvJvfSYHhhCidiRrjwC7mpng4oR7tOpXySOJKoj3/G+t32bDyyokQdIXCG5aoQm2KHMswEGDIQ1ZWdjfQ/bwG2lSJsFaXUECP9kl8fv7WQhpiU+DQJfjgSAOdJmoKQtWjnqfGyyNnr/X0Pn4XNjtErW6KYj7e4AvalBIV3GITHMVPeU9vg6k1wPeyN9j6b3Bn8GACmTvheMWKICwjlRP9kUk8p06eHgSeOBDKDrOLjRyo0yEf30p0cUrpplRvnMPXgd9FjTLhSbH+1dVsOo9/jbTrfmsrxW41Pdew/sWiYaeGyuEGNmlljcwdpLMgtMDCfploO2RUJND3x6lPmk6e30= 47 | - secure: XKwihW55kTwVRnrH1A5oP6R+qYe7tl+SFWeyw27tIDDOyVPyWbm2SgV3kQJFQ2P2kFia4E+wu3Rz4dsTSGvYAB4eFWA/msWUhPdpCE84Cwnqxpx+zkcTJ55+vxs4oau08C3Au/c7KRg6mPr08Go7X5+zZCkJ1/ff0joClRswQIfuE5Px7258xjNzk61ij+rehZnIKwisulcQvr+0O2icFyrVumg4CTdenN9h9u8ccvfCMdD6lwvtA7QV+xv7OZ9ezfx12hEUa6sQOr7XzHDbMZ32enU6TEvKpL8cq9YI2txv/B4IcTVSRwxWDVsqAgZODE9M2ZF6l1bmMTaH3Az23S8n7+SfuFRx6qhDPBOZ0M13su9oSWlUaya3gST4QvXvCZ+CHzdPHGG2rh5TW+K+QQXD52kmqyfXtkgA47aRI27c5iMuRojM4Dxm1I2r47dTYDq/0JIcIXlfqX2qd81uF0qEJ115Kmi71c64/jI7Ka6Aj2v6IXeanXvorP3lIOTIugF2oXDoPwP3+ESIfAfBbw9XhKh/uaKoTKkTKINX4R3Am2/n7/8vLw2zBGLso6X0S6Ro2DnTQ45hI3oCHbFxxAN0WT3orawuORgRjfGd5OlFNUd4Lnyy+1sZiFbe+KSlptYViUeD20lmxjwoOVzUxgtzIDSGH/da2zhINGG/GKI= 48 | deploy: 49 | provider: npm 50 | email: zak@zeroth.io 51 | skip_cleanup: true 52 | api_key: 53 | secure: f2Koc09HdhQ9LgmDhD4ZTNlcc35m2/yj4NCTxjNyDoKpT9auW6OdG22HC5HEXeRXzLOIcgR9r7f5CKXJOcSf4kU8g2Npj6kzYy0JPhYGc+MzsvQ5G/LCsMjCT6/Vm1dlGVqMvBlwQZ2h4vnj0ROZOrQ7pQTCpW4axLdllMrYvn6MaCvlPUCoEAgF6uzoiUesFEyVdaNQA5c8IFKhRcfBgT9X8c9MqcX0FRJc2O7+QojfYiamE5P62iwpmUNzgmDQytqkkT0BHFeLjOlZq/TJ24bk+3rQe7TTZEcfhTg+I4G8DAVxIKTHwN1/3ijHrg6n+pSt+oGQD37zwQJWmUWWlrcj7Bby5lRQCwmLL1cGqmHz3Opchg/SbO6AtMvXFbQRtJjewHgNUvXS6fNb0nYh89k43/9rWMeZ+FjSgvMCTfNzuZgAhQHzcyO6QvtByQpxu7GGwa0k4uxDrP0sr6Y0HxkP+8YkKYxtHnegPLAQasqm05AHJ9xzDUdJbkj6DJaV12eylOYfq4W8f7tbGJWiRVWWEToDCVNUt4KV4zd+HmCQf/PGtYY4UNv/e05cna8clQU8Ndb/xeV0a5/iA1UI/DRoZeVmTn7K7OkbCpWY+6kVTG/XPdAQ1t8frb+T1k1D6b9NEIhNgEjczWH3b4CUqlH1jrtHfnf7ioh25Brfz7k= 54 | on: 55 | branch: master 56 | tags: false 57 | -------------------------------------------------------------------------------- /.travis/deployment.key.pem.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zerothstack/zeroth/175a9117cdc9649f63b1777785a0039a8fe2a810/.travis/deployment.key.pem.enc -------------------------------------------------------------------------------- /DEVELOPER.md: -------------------------------------------------------------------------------- 1 | # Building and Testing Zeroth 2 | 3 | This document describes how to set up your development environment to build and test Zeroth. 4 | 5 | 6 | * [Prerequisite Software](#prerequisite-software) 7 | * [Getting the Sources](#getting-the-sources) 8 | * [Environment Variable Setup](#environment-variable-setup) 9 | * [Installing NPM Modules and Dart Packages](#installing-npm-modules-and-dart-packages) 10 | * [Build commands](#build-commands) 11 | * [Running Tests Locally](#running-tests-locally) 12 | * [Code Style](#code-style) 13 | * [Project Information](#project-information) 14 | * [CI using Travis](#ci-using-travis) 15 | * [Transforming Dart code](#transforming-dart-code) 16 | * [Debugging](#debugging) 17 | 18 | See the [contribution guidelines](https://github.com/zeroth/zeroth/blob/master/CONTRIBUTING.md) 19 | if you'd like to contribute to Zeroth. 20 | 21 | ## Prerequisite Software 22 | 23 | Before you can build and test Zeroth, you must install and configure the 24 | following products on your development machine: 25 | 26 | * [Git](http://git-scm.com) and/or the **GitHub app** (for [Mac](http://mac.github.com) or 27 | [Windows](http://windows.github.com)); [GitHub's Guide to Installing Git](https://help.github.com/articles/set-up-git) is a good source of information. 28 | 29 | * [Node.js](http://nodejs.org), (version `>=5.4.1 <6`) which is used to run a development web server, 30 | run tests, and generate distributable files. We also use Node's Package Manager, `npm` 31 | (version `>=3.5.3 <4.0`), which comes with Node. Depending on your system, you can install Node either from 32 | source or as a pre-packaged bundle. 33 | 34 | ## Getting the Sources 35 | 36 | Fork and clone the Zeroth repository: 37 | 38 | 1. Login to your GitHub account or create one by following the instructions given 39 | [here](https://github.com/signup/free). 40 | 2. [Fork](http://help.github.com/forking) the [main Zeroth 41 | repository](https://github.com/zeroth/zeroth). 42 | 3. Clone your fork of the Zeroth repository and define an `upstream` remote pointing back to 43 | the Zeroth repository that you forked in the first place. 44 | 45 | ```shell 46 | # Clone your GitHub repository: 47 | git clone git@github.com:/zeroth.git 48 | 49 | # Go to the Zeroth directory: 50 | cd zeroth 51 | 52 | # Add the main Zeroth repository as an upstream remote to your repository: 53 | git remote add upstream https://github.com/zeroth/zeroth.git 54 | ``` 55 | 56 | ## Installing NPM Modules and Dart Packages 57 | 58 | Next, install the JavaScript modules and Dart packages needed to build and test Zeroth: 59 | 60 | ```shell 61 | # Install Zeroth project dependencies (package.json) 62 | npm install 63 | ``` 64 | 65 | ## Build commands 66 | 67 | To build Zeroth and prepare tests, run: 68 | 69 | ```shell 70 | u build 71 | ``` 72 | 73 | Notes: 74 | * Library output is put in the `lib` folder. 75 | 76 | You can selectively test either the browser or server suites as follows: 77 | 78 | * `u build browser` 79 | * `u build server` 80 | 81 | To clean out the `dist` folder, run: 82 | 83 | ```shell 84 | u clean 85 | ``` 86 | 87 | ## Running Tests Locally 88 | 89 | ### Full test suite 90 | 91 | * `u test`: full test suite for both browser and server of Zeroth. These are the same tests 92 | that run on Travis. 93 | 94 | You can selectively run either environment as follows: 95 | 96 | * `u test server` 97 | * `u test browser` 98 | 99 | **Note**: If you want to only run a single test you can alter the test you wish to run by changing 100 | `it` to `iit` or `describe` to `ddescribe`. This will only run that individual test and make it 101 | much easier to debug. `xit` and `xdescribe` can also be useful to exclude a test and a group of 102 | tests respectively. 103 | 104 | ### Linting 105 | 106 | We use [tslint](https://github.com/palantir/tslint) for linting. See linting rules in [gulpfile](gulpfile.js). To lint, run 107 | 108 | ```shell 109 | $ u lint 110 | ``` 111 | 112 | ## Generating the API documentation 113 | 114 | The following gulp task will generate the API docs in the `dist-docs` directory: 115 | 116 | ```shell 117 | $ u doc build 118 | ``` 119 | 120 | You can serve the generated documentation to check how it would render on [zeroth.io](https://zeroth.io): 121 | ```shell 122 | $ u doc watch 123 | ``` 124 | 125 | Then open your browser to [http://localhost:8080](http://localhost:8080) to view the docs. Note that any edits to the 126 | markdown files that make up the docs will be live reloaded, but changes to assets or templates will need a restart to the 127 | doc watcher. 128 | 129 | *This document is modified from the [Angular 2 developer guide](https://github.com/angular/angular/blob/master/DEVELOPER.md)* 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2016 Zak Henry 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/zerothstack/zeroth.svg?branch=master)](https://travis-ci.org/zeroth/zeroth) 2 | [![Coverage Status](https://coveralls.io/repos/github/zerothstack/zeroth/badge.svg?branch=master)](https://coveralls.io/github/zerothstack/zeroth?branch=master) 3 | [![npm (scoped)](https://img.shields.io/npm/v/@zerothstack/core.svg?maxAge=2592000)](https://www.npmjs.com/package/@zerothstack/core) 4 | [![Join the chat at https://gitter.im/zerothstack/zeroth](https://badges.gitter.im/zerothstack/zeroth.svg)](https://gitter.im/zerothstack/zeroth?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | 6 | ![zeroth](https://rawgit.com/zerothstack/assets/master/logo/logotype.black.svg) 7 | 8 | # Core Module 9 | 10 | ## Overview 11 | Zeroth is a full stack Angular 2 framework. It uses the Angular 2 dependency injection pattern to bring good design patterns 12 | to the backend. Zeroth is an isomorphic framework, which means that components can be built to be shared between the frontend 13 | and the backend where appropriate. This significantly reduces code duplication and improves development speed. 14 | 15 | See http://zeroth.io for more info on this framework. 16 | 17 | This repo is for the core module only, and should not be forked to create a new project. To get started with a new Zeroth project, 18 | view the [Quickstart Guide](http://zeroth.io/guide/quick-start/) 19 | 20 | ## Note 21 | Zeroth is currently in alpha developer preview. Follow https://twitter.com/zeroth for updates, and check the [Roadmap](http://zeroth.io/#roadmap) for current status. 22 | 23 | ## Want to help? 24 | 25 | Want to file a bug, contribute some code, or improve documentation? Come join the fun! 26 | First, read up on our guidelines for [contributing][contributing] and then jump in. 27 | 28 | [contributing]: http://github.com/angular/angular/blob/master/CONTRIBUTING.md 29 | -------------------------------------------------------------------------------- /docker/common-services.yml: -------------------------------------------------------------------------------- 1 | # This common-services.yml definition holds all the image definitions and environment linking for the images. 2 | # It is intended to use the docker-compose extends functionality to use these definitions to build a docker-compose file 3 | version: "2" 4 | 5 | services: 6 | 7 | database: 8 | image: ${DB_IMAGE} 9 | 10 | cache: 11 | image: redis:3.0.7 12 | 13 | elasticsearch: 14 | image: elasticsearch:1.7.5 15 | 16 | mailcatcher: 17 | image: schickling/mailcatcher:latest 18 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zerothstack/zeroth/175a9117cdc9649f63b1777785a0039a8fe2a810/docs/.nojekyll -------------------------------------------------------------------------------- /docs/404.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: basic.hbs 3 | permalink: false 4 | --- 5 | 6 | ## OOPS! 7 | #### Looks like we can't find that page 8 | Try the sidebar links to see if you can find what you were looking for 9 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | zeroth.io 2 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: API 3 | date: 2016-05-29 4 | collection: main 5 | collectionSort: 1 6 | layout: api-container.hbs 7 | --- 8 | 9 | ## Error 10 | Typedoc output not found. Have you run the generator? 11 | ```bash 12 | $ z typedoc 13 | ``` 14 | -------------------------------------------------------------------------------- /docs/articles.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Articles 3 | collection: main 4 | collectionSort: 2 5 | layout: article-listing.hbs 6 | --- 7 | 8 | -------------------------------------------------------------------------------- /docs/assets/css/main.css: -------------------------------------------------------------------------------- 1 | #index-banner h1 { 2 | margin: 0 -30px; 3 | } 4 | 5 | #index-banner img { 6 | width: 400px; 7 | } 8 | -------------------------------------------------------------------------------- /docs/assets/image/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Logo 6 | 12 | 13 | -------------------------------------------------------------------------------- /docs/assets/image/logotype.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Logo 6 | 25 | 26 | -------------------------------------------------------------------------------- /docs/assets/image/title-card.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zerothstack/zeroth/175a9117cdc9649f63b1777785a0039a8fe2a810/docs/assets/image/title-card.jpg -------------------------------------------------------------------------------- /docs/assets/image/zeroth-cli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zerothstack/zeroth/175a9117cdc9649f63b1777785a0039a8fe2a810/docs/assets/image/zeroth-cli.png -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Changelog 3 | date: 2016-05-14 4 | collection: main 5 | collectionSort: 2 6 | layout: changelog.hbs 7 | changelog: CHANGELOG.md 8 | --- 9 | 10 | -------------------------------------------------------------------------------- /docs/guide.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Developer Guide 3 | date: 2016-05-29 4 | collection: main 5 | collectionSort: 0 6 | layout: guide-listing.hbs 7 | ------------------------- 8 | -------------------------------------------------------------------------------- /docs/guide/configuration.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Configuration 3 | description: We aren't going to choose your passwords for you! 4 | date: 2016-06-01 5 | collection: guide 6 | collectionSort: 1 7 | layout: guide.hbs 8 | --- 9 | 10 | ## Runtime configuration 11 | Zeroth uses a [dotenv] file to define configuration. This is exposed to `process.env.VAR_NAME` so you can easily access 12 | these variables anywhere. 13 | 14 | Only the variables that are prefixed with `PUBLIC_VAR_NAME` are exposed to `process.env.VAR_NAME` in the browser 15 | files - this is to avoid the mistake of passing in a secure variable like your database credentials to frontend code. 16 | 17 | Note that the variables [are replaced *at compile time*][define-plugin] in the browser, so there is no risk of gaining 18 | access to those variables with an injected script at runtime. 19 | 20 | ## Compile time configuration 21 | See the [cli documentation][compile-config] for details on how to customize the compile time 22 | 23 | [dotenv]: https://www.npmjs.com/package/dotenv 24 | [define-plugin]: https://webpack.github.io/docs/list-of-plugins.html#defineplugin 25 | [compile-config]: /guide/cli/#-zeroth-js-file-configuration 26 | -------------------------------------------------------------------------------- /docs/guide/controllers.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Controllers 3 | description: Take requests and shove them where they need to go. Send the results somewhere 4 | date: 2016-06-01 5 | collection: guide 6 | collectionSort: 1 7 | layout: guide.hbs 8 | --- 9 | 10 | Controllers in the Zeroth backend serve basically the same purpose as controllers in the frontend - they are the 11 | interface between services and the view layer. The difference being that in the backend, the view laye is the JSON 12 | serializer. 13 | 14 | ## Registration 15 | Controller registration is handled with the `@Controller()` class decorator. To ensure that the controller is loaded at 16 | bootstrap, the controllers are imported from the `./src/server/controllers/index.ts` [barrel] and passed into the bootstrap 17 | method: 18 | 19 | Example `./src/server/controller/test.controller.ts`: 20 | ```typescript 21 | import { Controller } from '@zerothstack/core/common'; 22 | import { AbstractController } from '@zerothstack/core/server'; 23 | 24 | @Controller({ 25 | routeBase: 'test', 26 | }) 27 | export class TestController extends AbstractController {...} 28 | 29 | ``` 30 | 31 | Example `./src/server/controllers/index.ts`: 32 | ```typescript 33 | export { TestController } from './test.controller'; 34 | ``` 35 | 36 | Example `./src/server/main.ts`: 37 | ```typescript 38 | import { bootstrap } from '@zerothstack/core/server'; 39 | import * as controllers from './controllers'; 40 | 41 | export { BootstrapResponse }; 42 | export default bootstrap([controllers], []); 43 | 44 | ``` 45 | 46 | ## Decorator parameters 47 | The `@Controller` decorator takes the following parameters to define the metadata on the controller: 48 | 49 | ### `routeBase` 50 | This defines the starting route segment for all method routes to start with. 51 | 52 | Example `./src/server/controller/test.controller.ts`: 53 | ```typescript 54 | import { Controller } from '@zerothstack/core/common'; 55 | import { AbstractController } from '@zerothstack/core/server'; 56 | 57 | @Controller({ 58 | routeBase: 'test', 59 | }) 60 | export class TestController extends AbstractController {...} 61 | 62 | ``` 63 | With the above controller, all routes will start with `/test` 64 | 65 | For more info on routing see the [routing] guide 66 | 67 | ## REST Methods 68 | As Zeroth is designed around the principles of good REST API patterns, the base `ResourceController` provides a number 69 | of methods for interacting with the resource that controller provides. 70 | 71 | ### `getOne` 72 | ```typescript 73 | @Route('GET', '/:id') 74 | getOne(request: Request, routeParams: RouteParamMap):Response; 75 | ``` 76 | 77 | ### `getMany` 78 | ```typescript 79 | @Route('GET', '/') 80 | getMany(request: Request, routeParams: RouteParamMap):Response; 81 | ``` 82 | ### `putOne` 83 | ```typescript 84 | @Route('PUT', '/:id') 85 | putOne(request: Request, routeParams: RouteParamMap):Response; 86 | ``` 87 | ### `putMany` *(planned)* 88 | ```typescript 89 | @Route('PUT', '/') 90 | putMany(request: Request, routeParams: RouteParamMap):Response; 91 | ``` 92 | ### `deleteOne` 93 | ```typescript 94 | @Route('DELETE', '/:id') 95 | deleteOne(request: Request, routeParams: RouteParamMap):Response; 96 | ``` 97 | ### `deleteMany` *(planned)* 98 | ```typescript 99 | @Route('DELETE', '/') 100 | deleteMany(request: Request, routeParams: RouteParamMap):Response; 101 | ``` 102 | ### `patchOne` 103 | ```typescript 104 | @Route('PATCH', '/:id') 105 | patchOne(request: Request, routeParams: RouteParamMap):Response; 106 | ``` 107 | ### `patchMany` *(planned)* 108 | ```typescript 109 | @Route('PATCH', '/') 110 | patchMany(request: Request, routeParams: RouteParamMap):Response; 111 | ``` 112 | 113 | You may note the absence of `POST` methods. This is intentional, as a well designed REST api that has distributed primary 114 | key generation (UUIDs) should never require a method that generates the id server-side. This is a core tenet of an 115 | [idempotent][http-idempotence] api - a request should be able to be replayed without there being a change in the state. 116 | 117 | If you *really* need `POST` requests, your extension of the `ResourceController` can always implement it's own methods 118 | with a `@Route('POST', '/')` decorator, however this is strongly discouraged. 119 | 120 | 121 | 122 | [barrel]: https://angular.io/docs/ts/latest/glossary.html#!#barrel 123 | [routing]: /guide/routing 124 | [http-idempotence]: https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods_and_web_applications 125 | -------------------------------------------------------------------------------- /docs/guide/database.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Database 3 | description: Poke your database in the ribs, carefully though - direct interaction is scary! 4 | date: 2016-06-01 5 | collection: guide 6 | collectionSort: 1 7 | layout: guide.hbs 8 | ----------------- 9 | 10 | ## Intro 11 | Zeroth makes connecting to and querying databases very simple. The connection configuration is all managed in the 12 | `.env` file. When working locally it is recommended to use Docker with Docker Compose to allow extremely simple startup 13 | and shutdown. 14 | 15 | Working with docker locally [will be simplified greatly][docker-issue] soon, but in the meantime it is simply a case of 16 | getting Docker running locally with Docker for [Mac][dfm]/[Windows][dfw]/[Linux][dfl] then running `docker-compose up -d` 17 | 18 | Working locally shouldn't have to be a chore to get a database stood up with the table structure and relevant dummy data 19 | entered. Instead, the Zeroth frameworks provides [migration] and [seeder] capabilities to manage building both your 20 | localhost database, and manage live database schema changes. 21 | 22 | ## Queries 23 | ### Model Stores & ORM 24 | Generally, controllers shouldn't interact directly with the `Database` dependency. Instead, they should interact with 25 | a [`ModelStore`][model-store] which extends the `DatabaseStore`. Classes that extend `DatabaseStore` have access 26 | to the TypeORM repository for the current model context, which allows powerful querying and manipulation of models. 27 | 28 | ### Raw Queries 29 | If you **really** need to make a direct database query, the `Database` service has a `getDriver()` method to directly 30 | interact with the database. This can be useful for more complex operations like doing batch operations that are managed 31 | within a transaction. 32 | 33 | ### Prepared Statements 34 | Prepared statements give SQL injection protection by signalling which parts of a query are variables and should be protected 35 | against. This execution is actually passed through to the native database driver - it is not handled in javascript at all. 36 | 37 | To prepare a statement, simply [tag an es6 template string][tagged-templates] with `Database.prepare`: 38 | ```typescript 39 | return this.database.query(Database.prepare`INSERT 40 | INTO books 41 | (name, author, isbn, category, recommended_age, pages, price) 42 | VALUES (${name}, ${author}, ${isbn}, ${category}, ${recommendedAge}, ${pages}, ${price}) 43 | `); 44 | ``` 45 | Take care to note that the `Database.prepare` tag does not wrap the string in parenthesis like a function call - it is 46 | ```typescript 47 | Database.prepare`template string ${templateVariable}` 48 | ``` 49 | Prepared statement string processing is provided by the [sql-template-strings] library 50 | 51 | ### Example complex database query 52 | This is an obviously contrived example but it demonstrates the use of transactions and prepared statements. 53 | ```typescript 54 | import { Injectable } from '@angular/core'; 55 | import { Database } from '@zerothstack/core/server'; 56 | import { Logger } from '@zerothstack/core/common'; 57 | 58 | class ExampleUtil { 59 | 60 | constructor(protected database: Database, protected logger: Logger) { 61 | } 62 | 63 | public flagLongUsernames(role: string, length: number): Promise { 64 | 65 | let driver: Driver; 66 | return this.database.getDriver() 67 | .then((d: Driver) => { 68 | driver = d; 69 | return driver.beginTransaction(); 70 | }) 71 | .then(() => this.database.query(Database.prepare`UPDATE users SET flagged = LENGTH(username) > ${length} WHERE role = ${role}`)) 72 | .then(() => driver.commitTransaction()) 73 | .catch(() => driver.rollbackTransaction()); 74 | 75 | } 76 | 77 | } 78 | ``` 79 | 80 | ## NoSQL support 81 | Zeroth is designed primarily to interact with a relational database. Support for NoSQL options like MongoDB are planned 82 | for the future, but not an immediate priority. 83 | 84 | [migration]: /guide/migrations 85 | [seeder]: /guide/seeders 86 | [docker-issue]: https://github.com/zerothstack/toolchain/issues/13 87 | [dfm]: https://docs.docker.com/docker-for-mac/ 88 | [dfw]: https://docs.docker.com/docker-for-windows/ 89 | [dfl]: https://docs.docker.com/engine/installation/linux/ 90 | [model-store]: /guide/model-stores/ 91 | [tagged-templates]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#Tagged_template_literals 92 | [sql-template-strings]: https://www.npmjs.com/package/sql-template-strings 93 | -------------------------------------------------------------------------------- /docs/guide/dependency-injection.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Dependency Injection 3 | description: Inject those dependencies everywhere, frontend, backend, anywhere! 4 | date: 2016-06-10 5 | collection: guide 6 | collectionSort: 1 7 | layout: guide.hbs 8 | --- 9 | 10 | Zeroth uses Angular 2's dependency injector. As one of the primary goals of Zeroth is to be able to share code between 11 | the frontend and backend, it makes sense to use the same injector for both. 12 | 13 | It is recommended to read the 14 | [Angular dependency injection guide](https://angular.io/docs/ts/latest/guide/dependency-injection.html), 15 | as nearly everything in there applies here, with the only exception being that there are no `@Component`s in the backend. 16 | For the backend, the Zeroth framework provides the following class decorators: 17 | * `@Model` 18 | * `@Controller` 19 | * `@Seeder` 20 | * `@Migration` 21 | * `@Store` 22 | * `@Service` 23 | 24 | Remember that the fronted of a Zeroth project *is Angular 2*, so it is important to have a firm grasp on how to work 25 | with dependency injection anyway, and that knowledge transfers directly to the backend. 26 | 27 | 28 | -------------------------------------------------------------------------------- /docs/guide/deployment.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Deployment 3 | description: Push your code to the real world, where it won't be lonely 4 | date: 2016-06-09 5 | collection: guide 6 | collectionSort: 1 7 | layout: guide.hbs 8 | --- 9 | 10 | ## Overview 11 | Deployment of a Zeroth application is not constrained by it's implementation. You can deploy it however you are used 12 | to deploying NodeJS & SPA applications, be it a bare metal server or a cloud provider like Heroku or your own large scale 13 | cloud data centre. 14 | 15 | When the code is compiled, the application is divided into two parts - the backend and the frontend. Note all common 16 | elements are actually compiled twice, once for each environment. 17 | 18 | For the browser side, it is a standard `index.html` file in the directory, and all the assets in folder alongside. 19 | You can simply serve this directory as you would any other single page application (remember to handle url rewriting). 20 | 21 | For the backend side, it is a standard NodeJS application, so use [forever] or similar to handle running the application. 22 | 23 | ## Config 24 | Remember that the configuration uses `.env` files *OR* global environment variables to manage runtime configuration. 25 | When deploying remember to export the appropriate variables or generate a `.env` file specially for production. 26 | 27 | The `.env` file is `.gitignore`d so you shouldn't make the mistake of committing sensitive data like database passwords 28 | to it. 29 | 30 | ## Future 31 | It is [currently planned][docker-deployment-issue] to implement integration with [Docker Cloud][docker-cloud] for one-command deployments 32 | to cloud services. 33 | 34 | [forever]:https://github.com/foreverjs/forever 35 | [docker-deployment-issue]: https://github.com/zerothstack/zeroth/issues/107 36 | [docker-cloud]: https://cloud.docker.com 37 | -------------------------------------------------------------------------------- /docs/guide/email.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Email 3 | description: Push slow notifications through the tubes 4 | date: 2016-06-09 5 | collection: guide 6 | collectionSort: 1 7 | layout: guide.hbs 8 | pendingTask: https://github.com/zerothstack/zeroth/issues/74 9 | --- 10 | -------------------------------------------------------------------------------- /docs/guide/exceptions.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Exceptions 3 | description: Something gone terribly wrong? Thow an exception 4 | date: 2016-06-20 5 | collection: guide 6 | collectionSort: 1 7 | layout: guide.hbs 8 | --- 9 | 10 | Zeroth provides a number of built-in `HttpExceptions` that should be thrown rather than `Error` when an unexpected 11 | event occurs. This allows the call stack handler to inspect the exception status code, and return that to the client so 12 | it can handle the error in a meaningful way. 13 | 14 | The exceptions that are available are: 15 | 16 | * BadRequestException (code 400) 17 | * UnauthorizedException (code 401) 18 | * PaymentRequiredException (code 402) 19 | * ForbiddenException (code 403) 20 | * NotFoundException (code 404) 21 | * MethodNotAllowedException (code 405) 22 | * NotAcceptableException (code 406) 23 | * ProxyAuthenticationRequiredException (code 407) 24 | * RequestTimeoutException (code 408) 25 | * ConflictException (code 409) 26 | * GoneException (code 410) 27 | * LengthRequiredException (code 411) 28 | * PreconditionFailedException (code 412) 29 | * PayloadTooLargeException (code 413) 30 | * URITooLongException (code 414) 31 | * UnsupportedMediaTypeException (code 415) 32 | * RangeNotSatisfiableException (code 416) 33 | * ExpectationFailedException (code 417) 34 | * UnprocessableEntityException (code 422) 35 | * TooManyRequestsException (code 429) 36 | * UnavailableForLegalReasonsException (code 451) 37 | * InternalServerErrorException (code 500) 38 | * NotImplementedException (code 501) 39 | * ServiceUnavailableException (code 503) 40 | * InsufficientStorageException (code 507) 41 | 42 | 43 | ## Example 44 | 45 | In a database store, the NotFoundException is thrown when the record is not present in the database: 46 | 47 | ```typescript 48 | public findOne(id: identifier): Promise { 49 | return this.orm.findByPrimary(id) 50 | .then((modelData: Instance): T => { 51 | if (!modelData){ 52 | throw new NotFoundException(`Model not found for id [${id}]`); 53 | } 54 | return new this.modelStatic(modelData.get()); 55 | }); 56 | } 57 | ``` 58 | 59 | The exception is eventually caught by the stack handler, and will return the following response: 60 | 61 | ``` 62 | HTTP/1.1 404 Not Found 63 | { 64 | "message": "Model not found for id [72eed629-c4ab-4520-a987-4ea26b134d8c]" 65 | } 66 | 67 | ``` 68 | -------------------------------------------------------------------------------- /docs/guide/migrations.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Migrations 3 | description: Turn those models into tables, but don't break the production database while you do that 4 | date: 2016-06-09 5 | collection: guide 6 | collectionSort: 1 7 | layout: guide.hbs 8 | pendingTask: https://github.com/zerothstack/zeroth/issues/25 9 | --- 10 | 11 | ## Overview 12 | Migrations are classes that represent a change in the database schema. They can either be a completely new table creation, 13 | or changes to tables like column renaming, addition, deletion, moving of data from one table to another. 14 | 15 | The important feature is that a migration should be reversible. This allows a mistake in a migration to be reversed safely 16 | without breaking the app. 17 | 18 | Sometime data loss is unavoidable, for example if you were to drop a column, the rollback can't recreate that lost data, 19 | but if the migration is reversed, the column will be recreated as it was, just without data. 20 | 21 | ## Registration 22 | Migrations can be registered using the `@Migration()` class decorator, and by extending the `AbstractMigration` abstract 23 | class, which provides the common interface for `migrate()` and `rollback()` 24 | 25 | Migrations classes must be imported by the server `main.ts` so that the decorator is invoked, registering the migration 26 | with the `EntityRegistry`. 27 | 28 | Example `./src/server/migrations/updateUsersUsername.migration.ts`: 29 | ```typescript 30 | import { AbstractMigration, Database } from '@zerothstack/core/server'; 31 | import { Migration, Logger } from '@zerothstack/core/common'; 32 | 33 | @Migration() 34 | export class UpdateUsersUsernameColumnMigration extends AbstractMigration { 35 | 36 | constructor(logger:Logger, database:Database){ 37 | super(logger, database); 38 | } 39 | 40 | public migrate(): Promise { 41 | return this.database.query(`ALTER TABLE users CHANGE username user VARCHAR(6) NOT NULL DEFAULT ''`); 42 | } 43 | 44 | public rollback():Promise { 45 | return this.database.query(`ALTER TABLE users CHANGE user username VARCHAR(6) NOT NULL DEFAULT ''`); 46 | } 47 | 48 | } 49 | ``` 50 | 51 | Example `./src/server/migrations/index.ts`: 52 | ```typescript 53 | export * from './updateUsersUsername.migration'; 54 | ``` 55 | 56 | Example `./src/server/main.ts`: 57 | ```typescript 58 | import { bootstrap } from '@zerothstack/core/server'; 59 | import * as migrations from './migrations'; 60 | 61 | export { BootstrapResponse }; 62 | export default bootstrap([migrations], []); 63 | 64 | ``` 65 | 66 | ## Current Status 67 | Migrations are currently a work in progress. They are current run immediately, and in parallel on bootstrap, which is ok for localhost 68 | rapid development, but won't work in production. 69 | 70 | The plan is to have the migrations register then have the `RemoteCli` pick up which migrations need to be run, and when 71 | the user logs in to the cli runtime they will be prompted for which migrations to run. 72 | 73 | See the [github issue](https://github.com/zerothstack/zeroth/issues/25) for more info 74 | -------------------------------------------------------------------------------- /docs/guide/models.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Models 3 | description: Give your data some shape, then make it a family by encouraging relationships 4 | date: 2016-06-09 5 | collection: guide 6 | collectionSort: 1 7 | layout: guide.hbs 8 | --- 9 | 10 | Models are the key shared components of the Zeroth framework. 11 | Typically, models represent a single row of a table, but this is by no means a limitation as a model can be any 12 | non-primitive data structure. 13 | 14 | ## Registration 15 | Models must be decorated with `@Model()` and loaded by the server main.ts file to ensure that their properties are 16 | registered with the [ModelStore][model-store]. 17 | 18 | All models should extend `AbstractModel`, which provides a contract of methods so other services can interact 19 | with an entity in a common way. 20 | 21 | Example `./src/common/models/example.model.ts`: 22 | ```typescript 23 | import { Model, AbstractModel } from '@zerothstack/core/common'; 24 | import { AbstractController } from '@zerothstack/core/server'; 25 | 26 | @Model() 27 | export class ExampleModel extends AbstractModel {...} 28 | 29 | ``` 30 | 31 | Example `./src/common/models/index.ts`: 32 | ```typescript 33 | export { TestController } from './test.controller'; 34 | ``` 35 | 36 | Example `./src/server/main.ts`: 37 | ```typescript 38 | import { bootstrap } from '@zerothstack/core/server'; 39 | import * as models from '../common/models'; 40 | 41 | export { BootstrapResponse }; 42 | export default bootstrap([models], []); 43 | 44 | ``` 45 | In the backend, when the bootstrapper for models runs, it iterates through all the models assigning their property 46 | definitions (defined by decorators) with the ORM. This is later used by the Migrations, Seeders and ModelStores to build 47 | the database schema and seed it with data. Also at runtime, thee definitions are used to determine mapping between the 48 | properties of the models and columns in the database. 49 | 50 | ## Manipulation 51 | In both the front and backend, models should be retrieved with a `Store`. In the backend, this will typically 52 | interact with the database to retrieve data, but it could also be interacting with remote APIs or filesystems etc. 53 | The key thing is that a controller that uses a `Store` *does not care* what the source is, which allows the store to be 54 | easily mocked for unit testing, or refactored to interact with a microservice rather than a database, for instance. 55 | 56 | For more detail on `Store`s and the methods they provide, see the [Model Store guide page][model-store]. 57 | 58 | ## Relationships 59 | *This feature has not yet been completed* 60 | 61 | ## See Also 62 | 63 | * [Model Validation](/guide/validation) 64 | 65 | [model-store]: /guide/model-stores 66 | -------------------------------------------------------------------------------- /docs/guide/queues.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Queues 3 | description: Procrastinate on slow processes. It'll get done later, promise. 4 | date: 2016-06-09 5 | collection: guide 6 | collectionSort: 1 7 | layout: guide.hbs 8 | pendingTask: https://github.com/zerothstack/zeroth/issues/76 9 | --- 10 | 11 | -------------------------------------------------------------------------------- /docs/guide/quick-start.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Quick Start 3 | description: Get up and running with your first Zeroth application as quick as possible. What are you waiting for? 4 | date: 2016-06-02 5 | collection: guide 6 | collectionSort: 0 7 | layout: guide.hbs 8 | ----------------- 9 | 10 | ## Prerequisites 11 | * Node installed (v6.9.1 is recommended) 12 | 13 | ## Installation 14 | 15 | Zeroth uses a commandline tool `z` to initialize a new project, and manage the building/watching/deployment/debugging etc within it's own shell. 16 | 17 | To get started, run the following command in your console *in an empty directory*: 18 | 19 | ```bash 20 | npm install -g @zerothstack/toolchain && z 21 | ``` 22 | 23 | This will install the toolchain then initialize a new project by cloning the boilerplate, and running installation. 24 | 25 | Once installation is complete, you will be taken on a brief tour of the features to get you familiarized with the cli. 26 | 27 | For more information, visit the [cli guide page](/guide/cli) 28 | -------------------------------------------------------------------------------- /docs/guide/routing.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Routing 3 | description: Make sure the right requests get to the right places. And the wrong ones don't! 4 | date: 2016-06-09 5 | collection: guide 6 | collectionSort: 1 7 | layout: guide.hbs 8 | --- 9 | 10 | ## Method routing 11 | Routing is defined in the backend controllers using the `@Route` decorator. 12 | 13 | In the following controller example, the route base is set to `example` and the method action is set to `/test`. 14 | 15 | ```typescript 16 | @Injectable() 17 | @Controller({ 18 | routeBase: 'example', 19 | }) 20 | export class TestController extends AbstractController { 21 | 22 | constructor(server: Server, logger: Logger) { 23 | super(server, logger); 24 | } 25 | 26 | @Route('GET', '/test') 27 | public testMethod(request: Request, response: Response): Response { 28 | return response.data('hello world'); 29 | } 30 | 31 | } 32 | ``` 33 | 34 | This configuration will result in the following route table being generated 35 | 36 | ``` 37 | ╔════════╤═════════════════════════╤═══════════════╗ 38 | ║ Method │ Path │ Stack ║ 39 | ╟────────┼─────────────────────────┼───────────────╢ 40 | ║ GET │ /api/example/test │ testMethod ║ 41 | ╚════════╧═════════════════════════╧═══════════════╝ 42 | ``` 43 | 44 | Note that the route is prefixed with `/api`. This is defined by the `./.env` file entry `PUBLIC_API_BASE=/api`. 45 | For more info on `.env` configuration, see the [Configuration guide](/guide/configuration) 46 | 47 | ### Route parameters 48 | Route params can be retrieved from the `request: Request` argument that is passed to controller methods and middleware. 49 | 50 | Example: 51 | ```typescript 52 | @Route('GET', '/test/:foo') 53 | public test(request: Request, response: Response): Response { 54 | this.logger.info(request.params().get('foo')); 55 | return response; 56 | } 57 | ``` 58 | When the above method is called, the `request.params()` returns a `Map` with key `foo` and value is whatever the http 59 | request value was. 60 | -------------------------------------------------------------------------------- /docs/guide/seeding.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Seeding 3 | description: Fill your database with not-quite-junk data for rapid development 4 | date: 2016-06-09 5 | collection: guide 6 | collectionSort: 1 7 | layout: guide.hbs 8 | --- 9 | 10 | ## Overview 11 | Seeders are classes that define mock data to fill the database with for development and QA purposes. 12 | 13 | When developing a new project locally, it can be quite a pain to set up your local database to have seed data that makes 14 | sense to work with either when testing backend REST API's or developing user interfaces. Also, when a new teammember 15 | starts working on the project, they have to go through a painful process of replicating your database. 16 | 17 | For QA testers, they need to go through the same process, and if some particularly curly bug occurs, it can sometimes 18 | be difficult to work out if it was the starting conditions, or an actual bug. 19 | 20 | When you add a new column, you are stuck with a choice of setting all rows to have the same value, or coming up with 21 | some method to seed this missing data individually. 22 | 23 | Seeders solve these issues by providing a repeatable, programmatic way of filling the database with realistic mock data, 24 | which can be destroyed and re-run when the database schema changes, generating new mock data. 25 | 26 | ## Registration 27 | Seeders are registered using the `@Seeder()` class decorator, and should all extend the `AbstractSeeder` class. 28 | This class provides the common interface that the `SeederBootstrapper` uses to start the seeder running. 29 | 30 | Example `./src/server/seeders/example.seeder.ts`: 31 | ```typescript 32 | import { UserStore } from '../../common/stores/user.store'; 33 | import { Logger, Collection, Seeder } from '@zerothstack/core/common'; 34 | import { AbstractSeeder } from '@zerothstack/core/server'; 35 | import { User } from '../../common/models/user.model'; 36 | import { UserMockStore } from '../../common/stores/user.mock.store'; 37 | import { UserDatabaseStore } from '../stores/user.db.store'; 38 | 39 | @Seeder() 40 | export class ExampleSeeder extends AbstractSeeder { 41 | 42 | constructor(loggerBase: Logger, protected userStore: UserStore, protected userMockStore: UserMockStore) { 43 | super(loggerBase); 44 | } 45 | 46 | public seed(): Promise { 47 | return this.userMockStore.findMany() 48 | .then((mockModels: Collection) => { 49 | 50 | return (this.userStore as UserDatabaseStore).getRepository() 51 | .then((repo: any) => repo.persistMany(...mockModels)); 52 | 53 | }); 54 | } 55 | 56 | } 57 | ``` 58 | 59 | Example `./src/server/seeders/index.ts`: 60 | ```typescript 61 | export * from './example.seeder'; 62 | ``` 63 | 64 | Example `./src/server/main.ts`: 65 | ```typescript 66 | import { bootstrap } from '@zerothstack/core/server'; 67 | import * as seeders from './seeders'; 68 | 69 | export { BootstrapResponse }; 70 | export default bootstrap([seeders], []); 71 | 72 | ``` 73 | 74 | 75 | ## Recommendations 76 | The naive approach to seeders would be to create a seeder for each table. However this does not generate realistic 77 | database data - is there actually a way that a single isolated record can be created in every one of your tables? Probably 78 | not. 79 | 80 | Instead, the recommended approach is to think about seeders as user stories. Say you have an application where you sell 81 | cars to a web user. Rather than creating a `CarSeeder`, `UserSeeder`, `CarFeatureSeeder`, `UserSelectionSeeder` etc, you 82 | will get more realistic data if you create a `UserSignupSeeder`, `AdminCarEntrySeeder`, `UserCarPurchaseSeeder`. 83 | 84 | Your goal when creating seeders should be to create as realistic data as is reasonable, so that when you are working with 85 | it later on, you are dealing with a `Toyota Hilux` purchased by `Gary Peterson`, rather than `Foo car type` purchased 86 | by `asdf1234`. Future you will thank current you ;) 87 | 88 | For more information on `MockStores` for mocking the actual model data, see the [mock store documentation][mock-stores] 89 | 90 | 91 | ## Current status 92 | Like [migrations], seeders are currently run on startup (immediately after migrations run). This is fine for local 93 | development, but is a slow process that you probably don't want running every time, even if you have a query check 94 | if a table has been seeded. 95 | 96 | In future, when migrations are moved into the runtime cli, seeders running will also be moved at the same time, and the 97 | cli will provide commands to run individual seeders. 98 | 99 | [migrations]: /guide/migrations 100 | [mock-stores]: /guide/model-stores/#mock-stores 101 | -------------------------------------------------------------------------------- /docs/guide/services.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Services 3 | description: Interact with other things and stuff 4 | date: 2016-06-09 5 | collection: guide 6 | collectionSort: 1 7 | layout: guide.hbs 8 | --- 9 | 10 | ## Intro 11 | Services are general-purpose singleton classes that can be injected by Controllers, Middleware, other services etc. 12 | A service can do anything from interacting with a third party API, to manipulating the local filesystem, to handling 13 | complex calculations. 14 | 15 | It reasonable to say that a service is anything that doesn't fit into the more rigid definitions of the other entities. 16 | 17 | ## Registration 18 | ### Basic 19 | If a service has no special startup requirements, it can simply be decorated with `@Injector()`, then be injected somewhere. 20 | 21 | Example: 22 | `./src/common/services/example.service.ts` 23 | ```typescript 24 | import { Injectable } from '@angular/core'; 25 | import { Logger } from '@zerothstack/core/common'; 26 | 27 | @Injectable() 28 | export class ExampleService { 29 | 30 | protected logger: Logger; 31 | 32 | constructor(loggerBase: Logger) { 33 | this.logger = loggerBase.source('Example Service'); 34 | } 35 | 36 | public logTest(message: string): void { 37 | this.logger.debug(message); 38 | } 39 | 40 | } 41 | ``` 42 | This service can be injected into any Controller by simply typehinting the service name: 43 | 44 | ```typescript 45 | constructor(protected exampleService: ExampleService) {} 46 | ``` 47 | The class will have a singleton instance of the `ExampleService` ready for action at `this.exampleService` 48 | 49 | ### Bootstrap blocking 50 | For more specialist services, the `@Service()` decorator is available which registers the service with the `EntityRegistry`, 51 | and is specially handled in the bootstrapper to defer bootstrapping of any other entity types until the `Service.initialize()` 52 | promise is resolved. 53 | 54 | These services should both be decorated with `@Service` *and* extend `AbstractService`, which provides the interface for 55 | the `initialized()` method. 56 | 57 | An (extremely contrived) example: 58 | ```typescript 59 | import { Injectable } from '@angular/core'; 60 | import { Logger, Service, AbstractService } from '@zerothstack/core/common'; 61 | import { lookup } from 'dns'; 62 | 63 | @Injectable() 64 | @Service() 65 | export class DnsService extends AbstractService { 66 | 67 | protected logger: Logger; 68 | 69 | constructor(loggerBase: Logger) { 70 | super(); 71 | this.logger = loggerBase.source('DNS Service'); 72 | } 73 | 74 | public lookup(host: string): Promise { 75 | return new Promise((resolve, reject) => { 76 | lookup(host, (err, address) => { 77 | if (err) { 78 | return reject(err); 79 | } 80 | return resolve(address); 81 | }) 82 | }); 83 | } 84 | 85 | public initialize(): Promise | this { 86 | return this.lookup('google.com') 87 | .then((address) => { 88 | this.logger.info(`Lookup passed, found address ${address}`); 89 | return this; 90 | }) 91 | .catch((e) => { 92 | this.logger.error(`Failed DNS lookup for Google, aborting bootstrap as either there is no internet connection or the world has ended`); 93 | throw e; 94 | }); 95 | } 96 | 97 | } 98 | ``` 99 | 100 | In general, you shouldn't need services that abort bootstrapping, but sometimes it is useful, especially if it is a service 101 | that you know it's failure will mean unexpected results if the server were to boot successfully. 102 | 103 | This bootstrap blocking is specific to the backend, and while services can be defined in the frontend or common sections, 104 | it has no effect on the frontend bootstrapper. 105 | -------------------------------------------------------------------------------- /docs/guide/testing.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Testing 3 | description: Make sure your beautiful code doesn't bite you later 4 | date: 2016-06-09 5 | collection: guide 6 | collectionSort: 1 7 | layout: guide.hbs 8 | --- 9 | 10 | ## Unit Testing 11 | ### Overview 12 | 13 | Unit testing in Zeroth follows the exact same pattern as Angular 2. As such, it uses the [Jasmine] BDD framework, with 14 | extra methods for dependency injection and handling of providers. 15 | 16 | ### Example with dependency injection 17 | 18 | ```typescript 19 | import { Logger, LoggerMock } from '@zerothstack/core/common'; 20 | import { addProviders, inject, async } from '@angular/core/testing'; 21 | import { Injectable } from '@angular/core'; 22 | import Spy = jasmine.Spy; 23 | 24 | @Injectable() 25 | class ExampleService { 26 | 27 | constructor(protected logger: Logger) {} 28 | 29 | public testLog(message: string): this { 30 | this.logger.debug(message); 31 | return this; 32 | } 33 | 34 | public testLogAsync(message: string): Promise { 35 | return Promise.resolve() 36 | .then(() => { 37 | this.testLog(message); 38 | return this; 39 | }); 40 | } 41 | 42 | } 43 | 44 | const providers = [ 45 | {provide: Logger, useClass: LoggerMock}, 46 | ExampleService 47 | ]; 48 | 49 | describe('Example service', () => { 50 | 51 | beforeEach(() => { 52 | addProviders(providers); 53 | }); 54 | 55 | it('logs messages passed to it', 56 | inject([ExampleService, Logger], 57 | (service: ExampleService, logger: Logger) => { 58 | 59 | const loggerSpy: Spy = spyOn(logger, 'persistLog'); 60 | 61 | service.testLog('hello world'); 62 | 63 | expect(loggerSpy).toHaveBeenCalledWith('debug', ['hello world']); 64 | })); 65 | 66 | it('logs messages passed to it after promise is resolved', 67 | async(inject([ExampleService, Logger], 68 | (service: ExampleService, logger: Logger) => { 69 | 70 | const loggerSpy: Spy = spyOn(logger, 'persistLog'); 71 | 72 | expect(loggerSpy).not.toHaveBeenCalledWith('debug', ['hello world async']); 73 | 74 | return service.testLogAsync('hello world async') 75 | .then(() => { 76 | expect(loggerSpy).toHaveBeenCalledWith('debug', ['hello world async']); 77 | }); 78 | 79 | }))); 80 | 81 | }); 82 | ``` 83 | 84 | 85 | [jasmine]: http://jasmine.github.io/2.4/introduction.html 86 | -------------------------------------------------------------------------------- /docs/guide/validation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Validation 3 | description: Don't trust anyone! Make sure input data is shiny 4 | date: 2016-06-09 5 | collection: guide 6 | collectionSort: 1 7 | layout: guide.hbs 8 | --- 9 | 10 | ## Overview 11 | 12 | Validation is provided by [class-validator] (built by [Umed Khudoiberdiev][@pleerock]) validation works on both browser 13 | side and server side, and is invoked with the `Store.validate` method. 14 | 15 | ## Usage 16 | You can use any of the methods in [class-validator], but make sure to import the methods from `@zerothstack/core/validation`. 17 | This is for two reasons: 18 | 1. You are importing from a location common to the core and your modules. This allows them to use the same `MetadataStorage` singleton 19 | and custom functions register to the same store. 20 | 2. Any custom methods defined in `@zerothstack/core` will be available from the same source. 21 | 22 | If you don't do this - property validation **will not register** and validation will pass on any field without a validator 23 | listed, so don't forget! 24 | 25 | ## Custom validators 26 | As described in [the class-validator docs][class-validator-custom] you can register custom validators. 27 | 28 | #### `@Injectable` validator classes 29 | The core has configured 30 | the validator to use the current `Injector`, so your class validators can inject dependencies too. 31 | 32 | For example: 33 | ```typescript 34 | import { Injectable } from '@angular/core'; 35 | import { Model } from '../models/model'; 36 | import { Validate, ValidatorConstraint, ValidatorConstraintInterface } from '@zerothstack/core/validation'; 37 | 38 | @Injectable() 39 | class TruthyService { 40 | 41 | public isTruthy(value: any): boolean { 42 | return !!value; 43 | } 44 | 45 | } 46 | 47 | @Injectable() 48 | @ValidatorConstraint() 49 | class CustomTruthyValidator implements ValidatorConstraintInterface { 50 | 51 | constructor(private validationService: TruthyService) { 52 | } 53 | 54 | public validate(value: any): boolean { 55 | 56 | return this.validationService.isTruthy(value); 57 | } 58 | 59 | } 60 | 61 | class Thing extends Model { 62 | 63 | @Validate(CustomTruthyValidator) 64 | public truthyValue: any; 65 | 66 | } 67 | ``` 68 | In this *(overly complex)* example the `Thing` model has a custom validator `CustomTruthyValidator` assigned to it. 69 | This validator implements the `ValidatorConstraintInterface` class-validator needs, which uses the injected `TruthyService`. 70 | 71 | ### Optional dependencies 72 | Sometimes you will want to register a custom validator that uses a dependency that is not available in all environments. 73 | If this is the case, make sure to decorate the injected paramater with `@Optional` so that the dependency injector know 74 | it can fail quietly if there is no registered injectable class. 75 | Make sure to return true in the `validate` function if the injector is not available: 76 | ```typescript 77 | import { Injectable, Optional } from '@angular/core'; 78 | import { ValidatorConstraint, ValidatorConstraintInterface } from '@zerothstack/core/validation'; 79 | 80 | @Injectable() 81 | @ValidatorConstraint() 82 | class RequiresServerValidator implements ValidatorConstraintInterface { 83 | 84 | constructor(@Optional private server: Server) { 85 | } 86 | 87 | public validate(value: any): boolean { 88 | if (!this.server){ 89 | return true; 90 | } 91 | return this.server.checkSomething(value); 92 | } 93 | 94 | } 95 | ``` 96 | 97 | It is recommended that you structure your application to avoid this however, as in general, you should be able to validate 98 | everything from the client side before sending data to the server. This makes for a significantly better user experience, 99 | as forms can show problems before they are submitted, not after. 100 | 101 | ## Async validators 102 | Any custom validator can return a `Promise` for asynchronous validation. Combined with dependency injection, 103 | custom validators can perform complex checks on a model. 104 | 105 | Example: 106 | ```typescript 107 | import { Injectable, Optional } from '@angular/core'; 108 | import { UserStore } from '../path/to/stores'; 109 | import { ValidatorConstraint, ValidatorConstraintInterface } from '@zerothstack/core/validation'; 110 | 111 | @Injectable() 112 | @ValidatorConstraint() 113 | class UsernameExistsValidator implements ValidatorConstraintInterface { 114 | 115 | constructor(private userStore: UserStore) { 116 | } 117 | 118 | public validate(value: any): Promise { 119 | return this.userStore.verifyUsernameExists(value); 120 | } 121 | 122 | } 123 | ``` 124 | 125 | [@pleerock]: https://github.com/pleerock 126 | [class-validator]: https://github.com/pleerock/class-validator 127 | [class-validator-custom]: https://github.com/pleerock/class-validator#custom-validation-classes 128 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home.hbs 3 | title: Zeroth 4 | description: Full stack isomorphic typescript framework. 5 | --- 6 | 7 | ## Roadmap 8 | 9 | ### First [beta](https://en.wikipedia.org/wiki/Software_release_life_cycle#Beta) release (feature complete) 10 | - [ ] Angular Universal (server side prerendering) integration 11 | - [ ] Docker deployment 12 | - [ ] Email sending 13 | - [ ] Queue handling 14 | - [ ] Pagination service (front & backend) 15 | - [ ] on initialization generate passwords and certificate for auth 16 | - [ ] Integrate code generation methods from Angular CLI 17 | - [ ] Docker integration into toolchain for local dev 18 | - [ ] CLI handled migrations 19 | - [x] CLI handled seeding 20 | - [x] Database migrations 21 | - [x] Database seeding from `MockModelProvider`s 22 | - [x] Full validation decorator complement 23 | - [x] 100% Code coverage 24 | - [x] Migration to TypeORM 25 | - [x] Entity registry with decorators for registering components anywhere 26 | 27 | ### Developer Preview [June 20th] 28 | - [x] Model hydration and mocking 29 | - [x] `ResourceController` implementation with CRUD routes 30 | - [x] Custom middleware registration 31 | - [x] Demo model schema sync and seed 32 | - [x] Http exceptions 33 | - [x] Post-initialization cli tour 34 | - [x] Full stack demo in quickstart 35 | - [x] FAQ page in docs 36 | - [x] Documented contribution guidelines 37 | 38 | ### Sprint 0 [June 10th] 39 | - [x] Full stack proof of concept 40 | - [x] Backend dependency injection with `@angular/core` 41 | - [x] Angular 2 compiling with webpack 42 | - [x] `webpack` watcher for browser & common changes with livereload 43 | - [x] `nodemon` watcher for server & common changes 44 | - [x] Compilation to es2015 for browser on build 45 | - [x] Compilation to es2015 for api on build 46 | - [x] Output consumable by `typings` 47 | - [x] Source mapping for api debugger 48 | - [x] Full stack debug breakpoints in Webstorm 49 | - [x] Live reloading browser 50 | - [x] Live restarting server 51 | - [x] Route registration with `@Route` decorator 52 | - [x] `docker-compose.json` running postgres db 53 | - [x] Connection to database from localhost with `Sequelize` 54 | - [x] Testing framework for both frontend and backend 55 | - [x] Travis Ci automated testing 56 | - [x] Coverage results remapped to typescript, pushed to coveralls 57 | - [x] Abstract and concrete `@injectable` implementation of `Logger` class/interface 58 | - [x] Automated deployment to npm registry with version bump 59 | - [x] Commandline interface 60 | - [x] `vantage` cli task runner integrating with `gulp` and `metalsmith` tasks 61 | - [x] Configuration file in parent project 62 | - [x] Extendable commands 63 | - [x] Shell connection to server runtime context 64 | - [x] Runtime task registration 65 | - [x] Initialization of new projects (quickstart) 66 | - [x] Documentation framework 67 | - [x] File watcher/livereload server 68 | - [x] Typedoc integration 69 | - [x] Extendable styles, templates, assets 70 | - [x] Default collections for automatic navigation generation 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@zerothstack/core", 3 | "version": "1.0.8", 4 | "description": "Zeroth core", 5 | "main": "index.js", 6 | "typings": "index.d.ts", 7 | "scripts": { 8 | "start": "z", 9 | "test": "z test -s", 10 | "coveralls": "z coveralls", 11 | "build": "z build", 12 | "preparepublish": "z build && cp -r lib/* .", 13 | "postpublish": "z changelog && z doc build && z typedoc && z deploy docs" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/zerothstack/zeroth.git" 18 | }, 19 | "keywords": [ 20 | "zeroth" 21 | ], 22 | "author": "Zak Henry (http://twitter.com/zak)", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/zerothstack/zeroth/issues" 26 | }, 27 | "homepage": "https://github.com/zerothstack/zeroth#readme", 28 | "dependencies": { 29 | "@angular/common": "2.1.2", 30 | "@angular/core": "2.1.2", 31 | "@angular/compiler": "2.1.2", 32 | "@angular/http": "2.1.2", 33 | "@angular/platform-browser": "2.1.2", 34 | "@angular/platform-browser-dynamic": "2.1.2", 35 | "@angular/platform-server": "2.1.2", 36 | "@types/chalk": "^0.4.28", 37 | "@types/chance": "^0.7.28", 38 | "@types/dotenv": "^2.0.17", 39 | "@types/express": "^4.0.29", 40 | "@types/hapi": "^13.0.28", 41 | "@types/jasmine": "^2.2.29", 42 | "@types/jsonwebtoken": "^5.7.28", 43 | "@types/lodash": "0.0.27", 44 | "@types/moment": "^2.11.28", 45 | "@types/node": "^4.0.30", 46 | "@types/proxyquire": "^1.3.26", 47 | "@types/socket.io": "^1.4.26", 48 | "@types/validator": "^4.5.26", 49 | "chalk": "^1.1.3", 50 | "chance": "^1.0.3", 51 | "class-validator": "0.6.4", 52 | "core-js": "^2.4.0", 53 | "dotenv": "^2.0.0", 54 | "express": "^4.13.4", 55 | "hapi": "^13.4.0", 56 | "jsonwebtoken": "^7.1.6", 57 | "lodash": "^4.12.0", 58 | "moment": "^2.13.0", 59 | "pg": "^6.1.0", 60 | "pg-hstore": "^2.3.2", 61 | "reflect-metadata": "^0.1.3", 62 | "rxjs": "5.0.0-beta.12", 63 | "sql-template-strings": "^2.0.3", 64 | "table": "^3.7.8", 65 | "timestamp": "0.0.1", 66 | "typeorm": "0.0.2-alpha.70", 67 | "vantage": "dthree/vantage#master", 68 | "zone.js": "^0.6.23" 69 | }, 70 | "devDependencies": { 71 | "@zerothstack/toolchain": "1.0.0", 72 | "proxyquire": "^1.7.10" 73 | }, 74 | "directories": { 75 | "doc": "docs" 76 | } 77 | } -------------------------------------------------------------------------------- /src/browser/bootstrap.spec.ts: -------------------------------------------------------------------------------- 1 | import {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from '@angular/platform-browser-dynamic/testing'; 2 | import { TestBed } from '@angular/core/testing'; 3 | 4 | TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); 5 | -------------------------------------------------------------------------------- /src/browser/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the browser-only module. It exports classes, functions and interfaces that are for the 3 | * browser implementations to use 4 | * 5 | * Any of the types (classes, functions etc) defined under this module can be imported from 6 | * `@zerothstack/core/browser` 7 | * 8 | * Example: 9 | * ```typescript 10 | * import { HttpStore } from '@zerothstack/core/browser'; 11 | * ``` 12 | * 13 | * @module browser 14 | * @preferred 15 | */ 16 | /** End Typedoc Module Declaration */ 17 | export * from './stores'; 18 | -------------------------------------------------------------------------------- /src/browser/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module browser 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import 'core-js'; 6 | import 'reflect-metadata'; 7 | require('zone.js/dist/zone'); 8 | if (process.env.ENV === 'production') { 9 | // Production 10 | } else { 11 | // Development 12 | Error['stackTraceLimit'] = Infinity; 13 | require('zone.js/dist/long-stack-trace-zone'); 14 | } 15 | -------------------------------------------------------------------------------- /src/browser/stores/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module browser 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | export * from './http.store'; 6 | -------------------------------------------------------------------------------- /src/browser/vendor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module browser 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import '@angular/platform-browser'; 6 | import '@angular/platform-browser-dynamic'; 7 | import '@angular/core'; 8 | import '@angular/common'; 9 | import '@angular/http'; 10 | // RxJS 11 | import 'rxjs/Rx'; 12 | // Other vendors for example jQuery, Lodash or Bootstrap 13 | // You can import js, ts, css, sass, ... 14 | -------------------------------------------------------------------------------- /src/common/exceptions/exceptions.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InsufficientStorageException, 3 | ServiceUnavailableException, 4 | NotImplementedException, 5 | InternalServerErrorException, 6 | UnavailableForLegalReasonsException, 7 | TooManyRequestsException, 8 | UnprocessableEntityException, 9 | ExpectationFailedException, 10 | RangeNotSatisfiableException, 11 | UnsupportedMediaTypeException, 12 | URITooLongException, 13 | PayloadTooLargeException, 14 | PreconditionFailedException, 15 | LengthRequiredException, 16 | GoneException, 17 | ConflictException, 18 | RequestTimeoutException, 19 | ProxyAuthenticationRequiredException, 20 | NotAcceptableException, 21 | MethodNotAllowedException, 22 | NotFoundException, 23 | ForbiddenException, 24 | PaymentRequiredException, 25 | UnauthorizedException, 26 | BadRequestException, 27 | HttpException, 28 | ValidationException 29 | } from './exceptions'; 30 | import { ValidationError } from 'class-validator'; 31 | 32 | describe('Exceptions', () => { 33 | 34 | const exceptions = [ 35 | {exception: BadRequestException, code: 400}, 36 | {exception: UnauthorizedException, code: 401}, 37 | {exception: PaymentRequiredException, code: 402}, 38 | {exception: ForbiddenException, code: 403}, 39 | {exception: NotFoundException, code: 404}, 40 | {exception: MethodNotAllowedException, code: 405}, 41 | {exception: NotAcceptableException, code: 406}, 42 | {exception: ProxyAuthenticationRequiredException, code: 407}, 43 | {exception: RequestTimeoutException, code: 408}, 44 | {exception: ConflictException, code: 409}, 45 | {exception: GoneException, code: 410}, 46 | {exception: LengthRequiredException, code: 411}, 47 | {exception: PreconditionFailedException, code: 412}, 48 | {exception: PayloadTooLargeException, code: 413}, 49 | {exception: URITooLongException, code: 414}, 50 | {exception: UnsupportedMediaTypeException, code: 415}, 51 | {exception: RangeNotSatisfiableException, code: 416}, 52 | {exception: ExpectationFailedException, code: 417}, 53 | {exception: UnprocessableEntityException, code: 422}, 54 | {exception: TooManyRequestsException, code: 429}, 55 | {exception: UnavailableForLegalReasonsException, code: 451}, 56 | {exception: InternalServerErrorException, code: 500}, 57 | {exception: NotImplementedException, code: 501}, 58 | {exception: ServiceUnavailableException, code: 503}, 59 | {exception: InsufficientStorageException, code: 507}, 60 | ]; 61 | 62 | exceptions.forEach((check) => { 63 | 64 | it(`creates instance of ${check.exception.prototype.constructor.name} with status code ${check.code}`, () => { 65 | 66 | const exceptionInstance = new check.exception; 67 | expect(exceptionInstance instanceof HttpException).toBe(true, 'instanceof HttpException'); 68 | expect(exceptionInstance instanceof Error).toBe(true, 'instanceof Error'); 69 | expect(exceptionInstance.getStatusCode()).toEqual(check.code); 70 | expect(exceptionInstance.name).toEqual(exceptionInstance.constructor.name); 71 | 72 | }); 73 | 74 | }); 75 | 76 | 77 | it('creates an exception that is an instance of it\'s parents', () => { 78 | 79 | let exception = new ValidationException(null, []); 80 | 81 | expect(exception instanceof HttpException).toBe(true); 82 | expect(exception instanceof UnprocessableEntityException).toBe(true); 83 | }); 84 | 85 | it('retrieves data from exception with .data()', () => { 86 | 87 | let errors:ValidationError[] = [{ 88 | target: null, 89 | property: 'name', 90 | constraints: { 91 | max_length: 'too long', 92 | }, 93 | value: 10, 94 | children: undefined, 95 | }]; 96 | 97 | let exception = new ValidationException(null, errors); 98 | 99 | expect(exception.getData()).toEqual(errors); 100 | 101 | }); 102 | 103 | }); 104 | -------------------------------------------------------------------------------- /src/common/exceptions/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module common 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | export * from './exceptions'; 6 | -------------------------------------------------------------------------------- /src/common/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | This is the common module - it exports all classes, functions and interfaces that can be used 3 | * across all platforms 4 | * 5 | * Any of the types (classes, functions etc) defined under this module can be imported from 6 | * `@zerothstack/core/common` 7 | * 8 | * Example: 9 | * ```typescript 10 | * import { Logger } from '@zerothstack/core/common'; 11 | * ``` 12 | * 13 | * @module common 14 | * @preferred 15 | */ 16 | /** End Typedoc Module Declaration */ 17 | export * from './models'; 18 | export * from './stores'; 19 | export * from './validation'; 20 | export * from './services'; 21 | export * from './registry'; 22 | export * from './exceptions'; 23 | -------------------------------------------------------------------------------- /src/common/metadata/metadata.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module common 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { TableOptions } from 'typeorm/decorator/options/TableOptions'; 6 | import { RelationType, Relation } from '../models/relations/index'; 7 | import { EntityMetadata, RegistryEntityStatic } from '../registry/entityRegistry'; 8 | import { ColumnOptions } from 'typeorm/decorator/options/ColumnOptions'; 9 | 10 | export interface PropertyDefinition { 11 | type: any; 12 | columnOptions?: ColumnOptions; 13 | } 14 | 15 | export interface ModelMetadata { 16 | storageKey?: string; 17 | tableOptions?: TableOptions; 18 | relations?: Map>>; 19 | storedProperties?: Map 20 | identifierKey?: string; 21 | timestamps?: { 22 | created?: string 23 | updated?: string 24 | } 25 | } 26 | 27 | /** 28 | * Common function used by many methods to ensure the entity has __metadata initialized on it's constructor 29 | * @param target 30 | */ 31 | export function initializeMetadata(target: RegistryEntityStatic) { 32 | if (!target.__metadata) { 33 | target.__metadata = {}; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/common/models/collection.spec.ts: -------------------------------------------------------------------------------- 1 | import { AbstractModel } from './model'; 2 | import { Collection } from './collection'; 3 | import { Primary } from './types/primary.decorator'; 4 | 5 | class BasicModel extends AbstractModel { 6 | 7 | @Primary() 8 | public id: number; 9 | } 10 | 11 | describe('Collection', () => { 12 | let collection: Collection; 13 | let data: BasicModel[]; 14 | beforeEach(() => { 15 | 16 | data = [ 17 | new BasicModel({id: 1}), 18 | new BasicModel({id: 2}), 19 | ]; 20 | 21 | collection = new Collection(data); 22 | }); 23 | 24 | it('can iterate over the collection with forEach', () => { 25 | collection.forEach((item, index) => { 26 | expect(item) 27 | .toEqual(data[index]); 28 | }); 29 | }); 30 | 31 | it('can iterate with for...of', () => { 32 | 33 | let count = 0; 34 | for (let item of collection) { 35 | count++; 36 | expect(item instanceof BasicModel) 37 | .toBe(true); 38 | } 39 | 40 | expect(count) 41 | .toEqual(collection.length); 42 | 43 | }); 44 | 45 | it('can find an item', () => { 46 | 47 | expect(collection.findById(2)) 48 | .toEqual(collection[1]); 49 | 50 | }); 51 | 52 | it('throws error when item can\'t be found', () => { 53 | let errorFind = () => collection.findById(3); 54 | expect(errorFind) 55 | .toThrowError(`Item with id [3] not in collection`); 56 | }); 57 | 58 | it('can check if an entity is present in the collection', () => { 59 | 60 | expect(collection.contains(data[1])) 61 | .toBe(true); 62 | expect(collection.contains(new BasicModel({id: 10}))) 63 | .toBe(false); 64 | 65 | }); 66 | 67 | }); 68 | -------------------------------------------------------------------------------- /src/common/models/collection.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module common 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { AbstractModel, identifier } from './model'; 6 | /** 7 | * Collection holds an array of [[AbstractModel|models]]. It provides common collection manipulation 8 | * methods for the controllers, services etc to work with models in an abstracted manner 9 | */ 10 | export class Collection extends Array { 11 | 12 | constructor(initialItems?: T[]) { 13 | super(); 14 | 15 | if (initialItems.length){ 16 | this.push.apply(this, initialItems); 17 | } 18 | } 19 | 20 | /** 21 | * Find an item in the collection by primary identifier 22 | * @param id 23 | * @returns {T} 24 | */ 25 | public findById(id: identifier): T { 26 | 27 | const found = this.find((model) => model.getIdentifier() === id); 28 | 29 | if (!found) { 30 | throw new Error(`Item with id [${id}] not in collection`); 31 | } 32 | 33 | return found; 34 | } 35 | 36 | /** 37 | * Remove an item from the collection 38 | * @param model 39 | */ 40 | public remove(model: T): void { 41 | const index: number = this.findIndex((item: T) => item.getIdentifier() === model.getIdentifier()); 42 | if (index >= 0) { 43 | this.splice(index, 1); 44 | } 45 | } 46 | 47 | /** 48 | * Check if the collection contains a given model 49 | * @param model 50 | * @returns {boolean} 51 | */ 52 | public contains(model: T): boolean { 53 | 54 | try { 55 | this.findById(model.getIdentifier()); 56 | return true; 57 | } catch (e) { 58 | } 59 | return false; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/common/models/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module common 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | export * from './model'; 6 | export * from './collection'; 7 | export * from './types'; 8 | export * from './relations'; 9 | -------------------------------------------------------------------------------- /src/common/models/model.spec.ts: -------------------------------------------------------------------------------- 1 | import { UUID, AbstractModel } from './model'; 2 | import { Primary } from './types/primary.decorator'; 3 | 4 | class BasicModel extends AbstractModel { 5 | 6 | @Primary() 7 | public id: string;//UUID; 8 | 9 | public stringNoDefault: string; 10 | public stringWithDefault: string = 'foo'; 11 | 12 | } 13 | 14 | describe('Model', () => { 15 | let instance: BasicModel; 16 | const id = 'f0d8368d-85e2-54fb-73c4-2d60374295e3'; 17 | beforeEach(() => { 18 | 19 | instance = new BasicModel({id}); 20 | }); 21 | 22 | it('hydrates a basic model', () => { 23 | 24 | expect(instance.id) 25 | .toEqual(id); 26 | }); 27 | 28 | it('retrieves the identifier with @Primary decorator', () => { 29 | expect(instance.getIdentifier()) 30 | .toEqual(id); 31 | }); 32 | 33 | it('retains default property values', () => { 34 | expect(instance.stringWithDefault) 35 | .toEqual('foo'); 36 | }); 37 | 38 | it('returns undefined for properties without default', () => { 39 | expect(instance.stringNoDefault) 40 | .toEqual(undefined); 41 | }); 42 | 43 | it('creates a model without properties if none are passed', () => { 44 | const model = new BasicModel; 45 | expect(Object.assign({}, model)).toEqual({stringWithDefault:'foo'}); 46 | }); 47 | 48 | }); 49 | 50 | describe('UUID', () => { 51 | 52 | it('extends String', () => { 53 | 54 | const id = new UUID('72eed629-c4ab-4520-a987-4ea26b134d8c'); 55 | 56 | expect(id instanceof UUID).toBe(true); 57 | expect(id instanceof String).toBe(true); 58 | }); 59 | 60 | }); 61 | -------------------------------------------------------------------------------- /src/common/models/model.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module common 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { RegistryEntityStatic, RegistryEntity } from '../registry/entityRegistry'; 6 | import { ModelMetadata } from '../metadata/metadata'; 7 | 8 | export type identifier = string | number | symbol; 9 | 10 | /** 11 | * Helper class for differentiating string keys with uuid keys 12 | */ 13 | export class UUID extends String { 14 | constructor(value?: any) { 15 | super(value); 16 | } 17 | } 18 | 19 | export interface ModelConstructor extends Function { 20 | constructor: ModelStatic; 21 | } 22 | 23 | export interface ModelStatic extends RegistryEntityStatic { 24 | new(data?: any, exists?: boolean): T; 25 | prototype: T; 26 | } 27 | 28 | /** 29 | * Common abstract class that **all** models must extend from. Provides common interfaces for other 30 | * services to interact with without knowing about the concrete implementation 31 | */ 32 | export abstract class AbstractModel extends RegistryEntity { 33 | 34 | constructor(data?: any) { 35 | super(); 36 | this.hydrate(data); 37 | } 38 | 39 | /** 40 | * Hydrates the model from given data 41 | * @param data 42 | * @returns {AbstractModel} 43 | */ 44 | protected hydrate(data: Object) { 45 | 46 | if (!data) { 47 | return this; 48 | } 49 | 50 | Object.assign(this, data); 51 | return this; 52 | } 53 | 54 | /** 55 | * Get the primary identifier of the model 56 | * @returns {any} 57 | */ 58 | public getIdentifier(): identifier { 59 | return this[this.getMetadata().identifierKey]; 60 | } 61 | 62 | } 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/common/models/relations/belongsTo.decorator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module common 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { ModelStatic, ModelConstructor, AbstractModel } from '../model'; 6 | import { 7 | initializeRelationMap, ForeignRelationModelGetter, Relation, 8 | ViaPropertyDefinition 9 | } from './index'; 10 | import { RelationOptions } from 'typeorm/decorator/options/RelationOptions'; 11 | 12 | /** 13 | * Defines the relationship between the current model and a foreign model via the decorated key 14 | * 15 | * Example: 16 | * ```typescript 17 | * 18 | * @Model 19 | * class Thumb extends AbstractModel { 20 | * 21 | * @BelongsTo(f => HandModel, hand => hand.handId) 22 | * public hand: HandModel; 23 | * 24 | * } 25 | * 26 | * ``` 27 | */ 28 | export function BelongsTo(foreignTypeGetter: ForeignRelationModelGetter, viaProperty:ViaPropertyDefinition, joinOptions?: RelationOptions): PropertyDecorator { 29 | return (target: ModelConstructor, propertyKey: string) => { 30 | initializeRelationMap(target, 'belongsTo'); 31 | 32 | target.constructor.__metadata.relations.get('belongsTo') 33 | .set(propertyKey, new Relation(target.constructor, foreignTypeGetter, viaProperty, joinOptions)); 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /src/common/models/relations/hand.model.fixture.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module common 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { HasOne } from './hasOne.decorator'; 6 | import { AbstractModel } from '../model'; 7 | import { Primary } from '../types/primary.decorator'; 8 | import { ThumbModel } from './thumb.model.fixture'; 9 | 10 | export class HandModel extends AbstractModel { 11 | 12 | @Primary() 13 | public handId: string;//UUID; 14 | 15 | public name: string; 16 | 17 | @HasOne(f => ThumbModel) 18 | thumb: ThumbModel; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/common/models/relations/hasOne.decorator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module common 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { ModelStatic, AbstractModel, ModelConstructor } from '../model'; 6 | import { initializeRelationMap, ForeignRelationModelGetter, Relation } from './index'; 7 | import { RelationOptions } from 'typeorm/decorator/options/RelationOptions'; 8 | 9 | /** 10 | * Defines the relationship between the current model and a foreign model via the decorated key 11 | * 12 | * Example: 13 | * ```typescript 14 | * 15 | * @Model 16 | * class Hand extends AbstractModel { 17 | * 18 | * @HasOne(f => ThumbModel) 19 | * public thumb: ThumbModel; 20 | * } 21 | * 22 | * ``` 23 | */ 24 | export function HasOne(foreignTypeGetter: ForeignRelationModelGetter, joinOptions?: RelationOptions): PropertyDecorator { 25 | return (target: ModelConstructor, propertyKey: string) => { 26 | initializeRelationMap(target, 'hasOne'); 27 | 28 | target.constructor.__metadata.relations.get('hasOne') 29 | .set(propertyKey, new Relation(target.constructor, foreignTypeGetter, null, joinOptions)); 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /src/common/models/relations/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module common 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { ModelStatic, ModelConstructor, AbstractModel } from '../model'; 6 | import { initializeMetadata } from '../../metadata/metadata'; 7 | 8 | export type RelationType = 'hasOne' | 'hasMany' | 'belongsTo' | 'belongsToMany'; 9 | 10 | /** 11 | * This is a crude method to two-way register the type of binding for relations. This is to overcome 12 | * a limitation of Typescripts design-time decorators and node's module resolution. 13 | * @see https://github.com/Microsoft/TypeScript/issues/4521 14 | */ 15 | export type ForeignRelationModelGetter = (thisStatic?: ModelStatic|any) => ModelStatic; 16 | 17 | export type ViaPropertyDefinition = (foreign: T) => any; 18 | 19 | export class Relation { 20 | 21 | constructor(public model: ModelStatic, 22 | private foreignRelationModelGetter: ForeignRelationModelGetter, 23 | public viaProperty: ViaPropertyDefinition, 24 | public databaseOptions?: any) { 25 | 26 | } 27 | 28 | public get foreign() { 29 | return this.foreignRelationModelGetter(this.model); 30 | } 31 | 32 | } 33 | 34 | /** 35 | * Initializes relation metadata property with empty values. Common function used by all relation 36 | * decorators to verify there is somewhere to assign their metadata. 37 | * @param target 38 | * @param type 39 | */ 40 | export function initializeRelationMap(target: ModelConstructor, type: RelationType) { 41 | initializeMetadata(target.constructor); 42 | 43 | if (!target.constructor.__metadata.relations) { 44 | target.constructor.__metadata.relations = new Map(); 45 | } 46 | 47 | if (!target.constructor.__metadata.relations.has(type)) { 48 | target.constructor.__metadata.relations.set(type, new Map()); 49 | } 50 | 51 | } 52 | 53 | export * from './hasOne.decorator'; 54 | export * from './belongsTo.decorator'; 55 | -------------------------------------------------------------------------------- /src/common/models/relations/relations.spec.ts: -------------------------------------------------------------------------------- 1 | import { ThumbModel } from './thumb.model.fixture'; 2 | import { HandModel } from './hand.model.fixture'; 3 | 4 | describe('Model Relations', () => { 5 | 6 | it('registers hasOne relationship with the constructor', () => { 7 | 8 | const model = new HandModel(); 9 | 10 | const relations = model.getMetadata().relations; 11 | expect(relations) 12 | .toBeDefined(); 13 | expect(relations.get('hasOne') 14 | .get('thumb').foreign) 15 | .toEqual(ThumbModel); 16 | 17 | }); 18 | 19 | it('registers belongsTo relationship with metadata', () => { 20 | 21 | const model = new ThumbModel(); 22 | 23 | const relations = model.getMetadata().relations; 24 | 25 | expect(relations.get('belongsTo') 26 | .get('hand').foreign) 27 | .toEqual(HandModel); 28 | 29 | expect(relations.get('belongsTo') 30 | .get('hand').viaProperty(new HandModel({handId:1}))) 31 | .toEqual(1); 32 | 33 | }); 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /src/common/models/relations/thumb.model.fixture.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module common 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { AbstractModel } from '../model'; 6 | import { Primary } from '../types/primary.decorator'; 7 | import { BelongsTo } from './belongsTo.decorator'; 8 | import { HandModel } from './hand.model.fixture'; 9 | 10 | export class ThumbModel extends AbstractModel { 11 | 12 | @Primary() 13 | public thumbId: string;//UUID; 14 | 15 | public name: string; 16 | 17 | @BelongsTo(f => HandModel, hand => hand.handId) 18 | public hand: HandModel; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/common/models/types/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module common 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | export * from './primary.decorator'; 6 | export * from './storedProperty.decorator'; 7 | export * from './timestamp.decorator'; 8 | -------------------------------------------------------------------------------- /src/common/models/types/primary.decorator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module common 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { ColumnOptions } from 'typeorm/decorator/options/ColumnOptions'; 6 | import { ModelConstructor } from '../model'; 7 | import { StoredProperty } from './storedProperty.decorator'; 8 | 9 | /** 10 | * @Primary property decorator for assigning which property is the primary identifier 11 | * @returns {function(ModelConstructor, string): void} 12 | * @constructor 13 | */ 14 | export function Primary(options?: ColumnOptions): PropertyDecorator { 15 | 16 | return function primary(target: ModelConstructor, propertyKey: string) { 17 | 18 | //associate with store properties so it doesn't need to be called twice 19 | StoredProperty(options)(target, propertyKey); 20 | 21 | target.constructor.__metadata.identifierKey = propertyKey; 22 | 23 | }; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/common/models/types/storedProperty.decorator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module common 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { ModelConstructor } from '../model'; 6 | import { ColumnOptions } from 'typeorm/decorator/options/ColumnOptions'; 7 | import { initializeMetadata } from '../../metadata/metadata'; 8 | 9 | /** 10 | * @StoredProperty property decorator for assigning the field type of the decorated property 11 | * @returns {function(ModelConstructor, string): void} 12 | * @constructor 13 | */ 14 | export function StoredProperty(options?: ColumnOptions): PropertyDecorator { 15 | 16 | return function storedProperty(target: ModelConstructor, propertyKey: string): void { 17 | 18 | initializeMetadata(target.constructor); 19 | 20 | if (!target.constructor.__metadata.storedProperties) { 21 | target.constructor.__metadata.storedProperties = new Map(); 22 | } 23 | 24 | let type = Reflect.getMetadata("design:type", target, propertyKey); 25 | target.constructor.__metadata.storedProperties.set(propertyKey, {type, columnOptions: options}); 26 | 27 | }; 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/common/models/types/timestamp.decorator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module common 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { ModelConstructor } from '../model'; 6 | import { initializeMetadata } from '../../metadata/metadata'; 7 | 8 | /** 9 | * Initializes the timestamps metadata property with empty values 10 | * @param target 11 | */ 12 | function initTimestamps(target: ModelConstructor) { 13 | initializeMetadata(target.constructor); 14 | 15 | if (!target.constructor.__metadata.timestamps) { 16 | target.constructor.__metadata.timestamps = {}; 17 | } 18 | } 19 | 20 | /** 21 | * @CreatedDate property decorator for assigning which property is to be defined as the created date 22 | * @returns {function(ModelConstructor, string): void} 23 | * @constructor 24 | */ 25 | export function CreatedDate(): PropertyDecorator { 26 | 27 | return function createdDate(target: ModelConstructor, propertyKey: string): void { 28 | initTimestamps(target); 29 | 30 | target.constructor.__metadata.timestamps.created = propertyKey; 31 | }; 32 | 33 | } 34 | 35 | /** 36 | * @UpdatedDate property decorator for assigning which property is to be defined as the updated date 37 | * @returns {function(ModelConstructor, string): void} 38 | * @constructor 39 | */ 40 | export function UpdatedDate(): PropertyDecorator { 41 | 42 | return function updatedDate(target: ModelConstructor, propertyKey: string): void { 43 | 44 | initTimestamps(target); 45 | 46 | target.constructor.__metadata.timestamps.updated = propertyKey; 47 | }; 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/common/registry/decorators.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module common 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { EntityType, EntityMetadata, EntityRegistry } from './entityRegistry'; 6 | import { ModelMetadata } from '../metadata/metadata'; 7 | 8 | /** 9 | * Common decorator factory function to simplify decorator declarations 10 | * @param type 11 | * @param metadata 12 | * @returns {function(TFunction): void} 13 | */ 14 | export function entityRegistryFunction(type: EntityType, metadata?: EntityMetadata): ClassDecorator { 15 | return function (target: TFunction): void { 16 | EntityRegistry.register(type, target, metadata); 17 | } 18 | } 19 | 20 | /** 21 | * @Model decorator for registering class with the [[EntityRegistry]] 22 | * 23 | * class Example: 24 | * ```typescript 25 | * 26 | * @Model() 27 | * export class ExampleModel extends AbstractModel {} 28 | * import { Model, AbstractModel } from '@zerothstack/core/common'; 29 | * ``` 30 | * @param metadata 31 | * @returns {ClassDecorator} 32 | * @constructor 33 | */ 34 | export function Model(metadata?: ModelMetadata): ClassDecorator { 35 | return entityRegistryFunction('model', metadata); 36 | } 37 | 38 | /** 39 | * @Store class decorator for registering class with the [[EntityRegistry]] 40 | * 41 | * Example: 42 | * ```typescript 43 | * import { Store, AbstractStore } from '@zerothstack/core/common'; 44 | * 45 | * @Store() 46 | * export class ExampleStore extends AbstractStore {} 47 | * ``` 48 | * @returns {ClassDecorator} 49 | * @constructor 50 | */ 51 | export function Store(): ClassDecorator { 52 | return entityRegistryFunction('store'); 53 | } 54 | 55 | /** 56 | * @Service class decorator for registering class with the [[EntityRegistry]] 57 | * 58 | * Example: 59 | * ```typescript 60 | * import { Service, AbstractService } from '@zerothstack/core/common'; 61 | * 62 | * @Service() 63 | * export class ExampleService extends AbstractService {} 64 | * ``` 65 | * @returns {ClassDecorator} 66 | * @constructor 67 | */ 68 | export function Service(): ClassDecorator { 69 | return entityRegistryFunction('service'); 70 | } 71 | -------------------------------------------------------------------------------- /src/common/registry/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module common 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | export * from './entityRegistry'; 6 | export * from './decorators'; 7 | -------------------------------------------------------------------------------- /src/common/services/consoleLogger.service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module common 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { Logger, LogLevel } from './logger.service'; 6 | import { yellow, red, bgRed, magenta, gray, blue } from 'chalk'; 7 | import { inspect } from 'util'; 8 | import { Injectable } from '@angular/core'; 9 | import * as moment from 'moment'; 10 | import { Service } from '../registry/decorators'; 11 | 12 | export const isBrowser = () => { 13 | return typeof window !== 'undefined'; 14 | }; 15 | 16 | /** 17 | * A concrete implementation of [[Logger]] this is a generic logger that will log to the console. 18 | * It can be used in both frontend and backend, and will log to the respective consoles. 19 | * Only in the NodeJS environment does colour highlighting take place 20 | */ 21 | @Service() 22 | @Injectable() 23 | export class ConsoleLogger extends Logger { 24 | 25 | private envBrowser:boolean; 26 | 27 | constructor() { 28 | super(ConsoleLogger); 29 | this.envBrowser = isBrowser(); 30 | } 31 | 32 | /** 33 | * Format the log with an appropriate colour 34 | * @param logLevel 35 | * @param message 36 | * @returns {string} 37 | */ 38 | public format(logLevel: LogLevel, message: string) { 39 | switch (logLevel) { 40 | case 'emergency': 41 | message = bgRed(message); 42 | break; 43 | case 'alert': 44 | message = red.underline(message); 45 | break; 46 | case 'critical': 47 | message = yellow.underline(message); 48 | break; 49 | case 'warning': 50 | message = yellow(message); 51 | break; 52 | case 'notice': 53 | message = magenta(message); 54 | break; 55 | case 'info': 56 | message = blue(message); 57 | break; 58 | case 'debug': 59 | message = gray(message); 60 | break; 61 | } 62 | 63 | return message; 64 | } 65 | 66 | /** 67 | * Format the messages - in node env anything that is not a string is passed into util.inspect 68 | * for coloured syntax highlighting 69 | * @param logLevel 70 | * @param messages 71 | * @returns {any} 72 | */ 73 | private formatMessages(logLevel: LogLevel, messages: any[]): any[] { 74 | // if in browser, defer to the browser for formatting 75 | if (this.envBrowser) { 76 | return messages; 77 | } 78 | 79 | return messages.map((message) => { 80 | switch (typeof message) { 81 | case 'string' : 82 | return this.format(logLevel, message); 83 | default: 84 | return inspect(message, { 85 | colors: true 86 | }); 87 | } 88 | }); 89 | } 90 | 91 | /** 92 | * Output the log to console. The log messages are prepended with the current time and source if 93 | * set 94 | * @param logLevel 95 | * @param messages 96 | * @returns {ConsoleLogger} 97 | */ 98 | public persistLog(logLevel: LogLevel, messages: any[]): this { 99 | 100 | messages = this.formatMessages(logLevel, messages); 101 | 102 | if (this.sourceName) { 103 | messages.unshift(gray('[' + this.format(logLevel, this.sourceName) + ']')); 104 | } 105 | 106 | messages.unshift(gray('[' + this.format(logLevel, moment() 107 | .format('HH:mm:ss')) + '] ')); 108 | 109 | switch (logLevel) { 110 | case 'emergency': 111 | case 'alert': 112 | case 'critical': 113 | case 'error': 114 | console.error(messages.shift(), ...messages); 115 | break; 116 | case 'warning': 117 | case 'notice': 118 | console.warn(messages.shift(), ...messages); 119 | break; 120 | default: 121 | console.log(messages.shift(), ...messages); 122 | } 123 | return this; 124 | }; 125 | 126 | } 127 | -------------------------------------------------------------------------------- /src/common/services/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module common 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | export * from './consoleLogger.service'; 6 | export * from './logger.service'; 7 | export * from './logger.service.mock'; 8 | export * from './service'; 9 | -------------------------------------------------------------------------------- /src/common/services/logger.service.mock.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module common 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { Logger, LogLevel } from './logger.service'; 6 | import { Injectable } from '@angular/core'; 7 | import { Service } from '../registry/decorators'; 8 | 9 | /** 10 | * Provides no-side effect mock for Logger for use in testing fixtures 11 | */ 12 | @Injectable() 13 | @Service() 14 | export class LoggerMock extends Logger { 15 | 16 | constructor() { 17 | super(LoggerMock); 18 | } 19 | 20 | /** 21 | * @inheritdoc 22 | */ 23 | public persistLog(logLevel: LogLevel, messages: any[]): this { 24 | return this; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/common/services/logger.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { inject, TestBed } from '@angular/core/testing'; 3 | import { LoggerMock } from './logger.service.mock'; 4 | import { Logger } from './logger.service'; 5 | 6 | 7 | @Injectable() 8 | class TestClass { 9 | 10 | constructor(public logger: Logger) {} 11 | 12 | } 13 | 14 | const providers = [ 15 | TestClass, 16 | {provide: Logger, useClass: LoggerMock}, 17 | ]; 18 | 19 | describe('Logger mock', () => { 20 | 21 | beforeEach(() => { 22 | TestBed.configureTestingModule({ providers }); 23 | }); 24 | 25 | it('Can be injected with the Logger token', inject([TestClass], (c: TestClass) => { 26 | 27 | let consoleSpy = spyOn(console, 'log'); 28 | 29 | expect(c instanceof TestClass) 30 | .toBe(true); 31 | expect(c.logger instanceof Logger) 32 | .toBe(true); 33 | expect(c.logger instanceof LoggerMock) 34 | .toBe(true); 35 | // expect(c.logger.debug() instanceof Logger) 36 | // .toBe(true); 37 | expect(consoleSpy) 38 | .not 39 | .toHaveBeenCalled(); 40 | 41 | })); 42 | }); 43 | -------------------------------------------------------------------------------- /src/common/services/service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module common 3 | */ 4 | /** 5 | * Common interface that all services *must* extend 6 | */ 7 | export abstract class AbstractService { 8 | 9 | /** 10 | * Method called at startup to defer bootstrapping of other components until resolved 11 | * @returns {Promise} 12 | */ 13 | public initialize():Promise | this { 14 | return Promise.resolve(this); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/common/stores/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module common 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | export * from './store'; 6 | export * from './mock.store'; 7 | -------------------------------------------------------------------------------- /src/common/stores/mock.store.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module common 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { identifier, ModelStatic, AbstractModel } from '../models/model'; 6 | import { Collection } from '../models/collection'; 7 | import { AbstractStore } from './store'; 8 | import { Injector } from '@angular/core'; 9 | import { Chance } from 'chance'; 10 | import * as _ from 'lodash'; 11 | 12 | /** 13 | * Provides abstract class to build concrete mock stores which can create new mock instances of 14 | * models. 15 | */ 16 | export abstract class MockStore extends AbstractStore { 17 | 18 | /** 19 | * Instance of chancejs 20 | * @see http://chancejs.com 21 | */ 22 | protected chanceInstance: Chance.Chance; 23 | 24 | protected modelCollection: Collection; 25 | 26 | constructor(modelStatic: ModelStatic, injector: Injector) { 27 | super(modelStatic, injector); 28 | 29 | this.initializeMockCollection(); 30 | } 31 | 32 | /** 33 | * Start the mock store off with some dummy data 34 | */ 35 | protected initializeMockCollection(): void { 36 | const models = _.times(10, () => this.getMock()); 37 | this.modelCollection = new Collection(models); 38 | } 39 | 40 | /** 41 | * Retrieve instance of chance, optionally providing a seed for the internal mersenne twister to 42 | * get repeatable random data 43 | * @param seed 44 | * @returns {ChanceInstance} 45 | */ 46 | protected chance(seed?: any): Chance.Chance { 47 | if (!this.chanceInstance || !!seed) { 48 | this.chanceInstance = new Chance(seed); 49 | } 50 | return this.chanceInstance; 51 | } 52 | 53 | /** 54 | * Get an instance of the model 55 | * @param id 56 | */ 57 | protected abstract getMock(id?: identifier): T; 58 | 59 | /** 60 | * @inheritdoc 61 | */ 62 | public async findOne(id?: identifier): Promise { 63 | 64 | try { 65 | return await this.modelCollection.findById(id); 66 | } catch (e) { 67 | return await this.saveOne(this.getMock(id)); 68 | } 69 | } 70 | 71 | /** 72 | * @inheritdoc 73 | */ 74 | public findMany(query?: any): Promise> { 75 | return Promise.resolve(this.modelCollection); 76 | } 77 | 78 | /** 79 | * Mock saving the model 80 | * 81 | * As saving does not make sense for a mock store, this just stubs the interface by returning 82 | * the model in a resolved promise 83 | */ 84 | public saveOne(model: T): Promise { 85 | this.modelCollection.push(model); 86 | return Promise.resolve(model); 87 | } 88 | 89 | /** 90 | * Mock selecting model by id 91 | * 92 | * As deleting does not make sense for a mock store, this just stubs the interface by returning 93 | * the model in a resolved promise 94 | * @param model 95 | * @returns {Promise} 96 | */ 97 | public deleteOne(model: T): Promise { 98 | this.modelCollection.remove(model); 99 | return Promise.resolve(model); 100 | } 101 | 102 | /** 103 | * @inheritdoc 104 | */ 105 | public hasOne(model: T): Promise { 106 | return Promise.resolve(this.modelCollection.contains(model)); 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /src/common/stores/store.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module common 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { identifier, ModelStatic, AbstractModel } from '../models/model'; 6 | import { Injector } from '@angular/core'; 7 | import { Collection } from '../models/collection'; 8 | import { ValidatorOptions, ValidationError, getValidator, Validator } from '../validation'; 9 | import { ValidationException } from '../exceptions/exceptions'; 10 | 11 | export interface Query { 12 | } 13 | 14 | /** 15 | * The abstract store should be the root calls for *all* stores, it provides common methods 16 | * for entity validation and storage. 17 | */ 18 | export abstract class AbstractStore { 19 | 20 | /** 21 | * class-validator Validator instance 22 | * @see https://github.com/pleerock/class-validator 23 | */ 24 | protected validator: Validator; 25 | 26 | constructor(protected modelStatic: ModelStatic, protected injector: Injector) { 27 | 28 | this.validator = getValidator(injector); 29 | } 30 | 31 | /** 32 | * Promise that store is initialized. 33 | * Override this function for stores that have async initialization like Database stores that 34 | * require a connection etc. 35 | * @returns {Promise} 36 | */ 37 | public initialized(): Promise { 38 | return Promise.resolve(this); 39 | } 40 | 41 | /** 42 | * Find one instance by id 43 | * @param id 44 | * @returns {Promise} 45 | */ 46 | public abstract findOne(id: identifier): Promise; 47 | 48 | /** 49 | * Save the model. Depending on the implementation, this may be a partial save when the model 50 | * is known to exist in the store destination and only an update is needed 51 | * @param model 52 | * @returns {Promise} 53 | */ 54 | public abstract saveOne(model: T): Promise; 55 | 56 | /** 57 | * Check if a model exists in the database 58 | * @param model 59 | */ 60 | public abstract hasOne(model: T): Promise; 61 | 62 | /** 63 | * Delete the model from the store. 64 | * @param model 65 | */ 66 | public abstract deleteOne(model: T): Promise; 67 | 68 | /** 69 | * Find multiple entities using a query for constraints 70 | * @param query 71 | * @returns {Promise>} 72 | */ 73 | public abstract findMany(query?: Query): Promise>; 74 | 75 | // public abstract saveMany(models:Collection): Promise>; 76 | // public abstract deleteMany(models:Collection): Promise; 77 | 78 | /** 79 | * Check the entity passed is valid, if not throw ValidationException with the errors 80 | * @param model 81 | * @param validatorOptions 82 | * @returns {Promise} 83 | */ 84 | public async validate(model: T, validatorOptions?: ValidatorOptions): Promise | never { 85 | 86 | const errors: ValidationError[] = await this.validator.validate(model, validatorOptions); 87 | if (errors.length) { 88 | throw new ValidationException(null, errors) 89 | } 90 | return model; 91 | } 92 | 93 | /** 94 | * Build the entity from data 95 | * @param modelData 96 | * @returns {Promise} 97 | */ 98 | public hydrate(modelData: any): Promise { 99 | return Promise.resolve(new this.modelStatic(modelData)); 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/common/util/banner.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module common 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | 6 | import * as chalk from 'chalk'; 7 | 8 | /** 9 | * Zeroth Banner for usage in cli welcome 10 | * @type {string} 11 | */ 12 | export const banner = ` 13 | 0 14 | 00 00000 15 | 0000 00 0000 16 | 00000 0000 00000 17 | 0000 000000 0000 18 | 000 0 000 000 19 | 000 00 000 000 20 | 000 000000 00 000 21 | 000 000 Zeroth 000 000 22 | 000 00 000000 000 23 | 000 000 00 000 24 | 000 000 0 000 25 | 0000 000000 0000 26 | 00000 000 00000 27 | 0000 000 0000 28 | 00000 00 29 | 0`; 30 | 31 | export function bannerBg(message: string = '$ Zeroth Runtime CLI', bgString: string): string { 32 | 33 | let shortMessage: string = ''; 34 | let longMessage: string = ''; 35 | 36 | message = ` ${message} `; 37 | 38 | message.length > 36 ? longMessage = message : shortMessage = message; 39 | 40 | shortMessage += "*".repeat(38 - shortMessage.length); 41 | 42 | const template = ` 43 | ************************************** ************************************** 44 | ********************************* 0 ********************************* 45 | ****************************** 00 00000 ***************************** 46 | ************************** 0000 00 0000 ************************* 47 | *********************** 00000 0000 00000 ********************** 48 | *********************** 0000 000000 0000 ********************* 49 | ********************** 000 0 000 000 ********************* 50 | ********************** 000 00 000 000 ********************* 51 | ********************** 000 000000 00 000 ********************* 52 | ********************** 000 000 Zeroth 000 000 ********************* 53 | ********************** 000 00 000000 000 ********************* 54 | ********************** 000 000 00 000 ********************* 55 | ********************** 000 000 0 000 ********************* 56 | *********************** 0000 000000 0000 ********************** 57 | *********************** 00000 000 00000 ********************** 58 | ************************** 0000 000 0000 ************************* 59 | ****************************** 00000 00 ***************************** 60 | ********************************** 0 ******************************** 61 | *************************************** ************************************* 62 | ******************************************************************************* 63 | ${longMessage}`; 64 | 65 | const minReplacementLength = template.match(/\*+/g) 66 | .join('').length; 67 | if (bgString.length < minReplacementLength) { 68 | bgString = bgString.repeat(minReplacementLength / bgString.length + 1); 69 | } 70 | 71 | return template 72 | .replace(/0/g, '\u2b21') 73 | .replace(/\*+/g, (match: string) => { 74 | const replacement: string = bgString.substr(0, match.length); 75 | 76 | bgString = bgString.substr(match.length); 77 | return chalk.gray(replacement); 78 | }); 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/common/util/serialPromise.spec.ts: -------------------------------------------------------------------------------- 1 | import { serialPromise } from './serialPromise'; 2 | 3 | describe('Process promises sequentially', () => { 4 | 5 | it('should be able to run a series of promises in sequence', () => { 6 | 7 | let promiseFactories = [ 8 | (value: number) => Promise.resolve(value + 1), 9 | (value: number) => Promise.resolve(value + 2), 10 | (value: number) => Promise.resolve(value + 3), 11 | ]; 12 | 13 | let promiseResult = serialPromise(promiseFactories, 0); 14 | 15 | promiseResult.then((result) => { 16 | expect(result) 17 | .toEqual(6); 18 | }); 19 | 20 | }); 21 | 22 | it('should be able to run a series of promises in sequence with extra arguments', () => { 23 | 24 | let promiseFactories = [ 25 | (value: number, extra: number) => Promise.resolve(value + 1 + extra), 26 | (value: number, extra: number) => Promise.resolve(value + 2 + extra), 27 | (value: number, extra: number) => Promise.resolve(value + 3 + extra), 28 | ]; 29 | 30 | let promiseResult = serialPromise(promiseFactories, 0, null, 2); 31 | 32 | return promiseResult.then((result) => { 33 | expect(result) 34 | .toEqual(12); 35 | }); 36 | }); 37 | 38 | }); 39 | -------------------------------------------------------------------------------- /src/common/util/serialPromise.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module common 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | export interface PromiseFactory { 6 | (arg?: any, ...args: any[]): T | Promise; 7 | } 8 | 9 | /** 10 | * Invoke a series of promise factories one after the other, only kicking off the next promise 11 | * when the prior one resolves. 12 | * @param promiseFactories 13 | * @returns {Promise} 14 | * @param thisArg 15 | * @param args 16 | * @param initialValue 17 | */ 18 | export function serialPromise(promiseFactories: PromiseFactory[], initialValue:T, thisArg: any = null, ...args: any[]): Promise { 19 | return promiseFactories.reduce((soFar: Promise, next: PromiseFactory): Promise => { 20 | 21 | return soFar.then((result): Promise => { 22 | return Promise.resolve(next.call(thisArg, result, ...args)); 23 | }); 24 | 25 | }, Promise.resolve(initialValue)); 26 | } 27 | -------------------------------------------------------------------------------- /src/common/validation/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module common 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { Injector } from '@angular/core'; 6 | import { useContainer, Validator } from 'class-validator'; 7 | 8 | interface InjectableClass { 9 | new (...args: any[]): T 10 | } 11 | /** 12 | * class-validator has a basic inversion container that handles classes without constructor arguments 13 | * This hybrid class re-implements that injector, and extends it to be able to use the core angular 14 | * injector when available. 15 | */ 16 | class HybridInjector { 17 | 18 | public injector: Injector; 19 | 20 | private instances: WeakMap, any> = new WeakMap(); 21 | 22 | /** 23 | * Get the instance of a class when there is no dependency 24 | * @param staticClass 25 | * @returns {any} 26 | */ 27 | public noDependencyGet(staticClass: InjectableClass): T { 28 | 29 | if (!this.instances.has(staticClass)){ 30 | this.instances.set(staticClass, new staticClass()); 31 | } 32 | 33 | return this.instances.get(staticClass); 34 | } 35 | 36 | public get(staticClass: InjectableClass): T { 37 | try { 38 | if (!this.injector) { 39 | return this.noDependencyGet(staticClass); 40 | } 41 | 42 | return this.injector.get(staticClass); 43 | } catch (e) { 44 | return this.noDependencyGet(staticClass); 45 | } 46 | } 47 | } 48 | 49 | const hybridInjectorInstance: HybridInjector = new HybridInjector(); 50 | useContainer(hybridInjectorInstance); 51 | 52 | /** 53 | * Get the (singleton) validator instance and assign the dependency injector so custom validators 54 | * can use the DI service. 55 | * @param injector 56 | * @returns {Validator} 57 | */ 58 | export function getValidator(injector: Injector): Validator { 59 | hybridInjectorInstance.injector = injector; 60 | 61 | return hybridInjectorInstance.get(Validator); 62 | } 63 | 64 | // This common export is used so that both the core and the implementing modules 65 | // use the same instance. This is necessary as class-validator does a single export 66 | // of the MetadataStorage class which is used as a singleton. Otherwise, the models 67 | // may register their validations against a different store than what is validated with 68 | export * from 'class-validator'; 69 | -------------------------------------------------------------------------------- /src/server/bootstrap.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import {platformServerTesting, ServerTestingModule} from '@angular/platform-server/testing'; 3 | import { TestBed } from '@angular/core/testing'; 4 | 5 | TestBed.initTestEnvironment(ServerTestingModule, platformServerTesting()); 6 | console.log('initialised test env'); 7 | -------------------------------------------------------------------------------- /src/server/bootstrap/controllers.bootstrapper.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { Injectable } from '@angular/core'; 3 | import { RemoteCliMock } from '../services/remoteCli.service.mock'; 4 | import { RemoteCli } from '../services/remoteCli.service'; 5 | import { ServerMock } from '../servers/abstract.server.mock'; 6 | import { Logger } from '../../common/services/logger.service'; 7 | import { LoggerMock } from '../../common/services/logger.service.mock'; 8 | import { Server } from '../servers/abstract.server'; 9 | import { bootstrap, BootstrapResponse } from './bootstrap'; 10 | import { EntityRegistry } from '../../common/registry/entityRegistry'; 11 | import { AbstractController } from '../controllers/abstract.controller'; 12 | import { Route } from '../controllers/route.decorator'; 13 | import { Request } from '../controllers/request'; 14 | import { Response } from '../controllers/response'; 15 | import { AuthServiceMock } from '../services/auth.service.mock'; 16 | import { AuthService } from '../services/auth.service'; 17 | 18 | const providers: any[] = [ 19 | {provide: Logger, useClass: LoggerMock}, 20 | {provide: Server, useClass: ServerMock}, 21 | {provide: RemoteCli, useClass: RemoteCliMock}, 22 | {provide: AuthService, useClass: AuthServiceMock}, 23 | ]; 24 | 25 | @Injectable() 26 | class TestController extends AbstractController { 27 | 28 | constructor(logger: Logger) { 29 | super(logger); 30 | } 31 | 32 | @Route('GET', '/test') 33 | public test(request: Request, response: Response): any { 34 | } 35 | 36 | } 37 | 38 | describe('Controller Bootstrapper', () => { 39 | 40 | beforeEach(() => { 41 | TestBed.configureTestingModule({ providers }); 42 | EntityRegistry.clearAll(); 43 | 44 | EntityRegistry.register('controller', TestController); 45 | 46 | }); 47 | 48 | it('resolves and initializes controller with injector and registers routes with server', (done: Function) => { 49 | 50 | const result = bootstrap(undefined, providers)(); 51 | 52 | return result.then((res: BootstrapResponse) => { 53 | 54 | const routes = res.server.configuredRoutes; 55 | 56 | const controller = res.injector.get(TestController); 57 | 58 | expect(controller) 59 | .toBeDefined(); 60 | expect(controller instanceof TestController) 61 | .toBe(true, 'Instance is not a TestController'); 62 | expect(routes[0].methodName) 63 | .toEqual('test'); 64 | 65 | done(); 66 | 67 | }); 68 | 69 | }); 70 | 71 | }); 72 | -------------------------------------------------------------------------------- /src/server/bootstrap/controllers.bootstrapper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module server 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { EntityBootstrapper } from './entity.bootstrapper'; 6 | import { AbstractController } from '../controllers/abstract.controller'; 7 | import { RegistryEntityStatic } from '../../common/registry/entityRegistry'; 8 | import { Server } from '../servers/abstract.server'; 9 | import { ControllerMetadata } from '../registry/decorators'; 10 | 11 | /** 12 | * Provides bootstrapping of the @[[Controller]] entities 13 | */ 14 | export class ControllerBootstrapper extends EntityBootstrapper { 15 | 16 | /** 17 | * Returns all controllers registered to the [[EntityRegistry]] 18 | */ 19 | public getInjectableEntities(): RegistryEntityStatic[] { 20 | return this.getEntitiesFromRegistry('controller'); 21 | } 22 | 23 | public bootstrap(): void { 24 | const server = this.injector.get(Server); 25 | this.entities.forEach((resolvedController: RegistryEntityStatic) => { 26 | 27 | let controller = this.getInstance(resolvedController); 28 | 29 | controller.registerInjector(this.injector) 30 | .registerRoutes(server); 31 | 32 | }); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/server/bootstrap/entity.bootstrapper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module server 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { ReflectiveInjector } from '@angular/core'; 6 | import { Logger } from '../../common/services/logger.service'; 7 | import { 8 | EntityType, RegistryEntityStatic, 9 | EntityMetadata, EntityRegistry 10 | } from '../../common/registry/entityRegistry'; 11 | 12 | /** 13 | * Provides abstract class for all bootstrappers to extend with, common interface for the 14 | * bootstrap function to have a single pattern to invoke the bootstrappers 15 | */ 16 | export abstract class EntityBootstrapper { 17 | 18 | /** Array of all entities retrieved from the [[EntityRegistry]] */ 19 | protected entities: RegistryEntityStatic[]; 20 | /** Reference to the Injector instance. Can be set with [[setInjector]]*/ 21 | protected injector: ReflectiveInjector; 22 | /** Instance of Logger, initialized with the current implementations class name as source */ 23 | protected logger: Logger; 24 | 25 | /** 26 | * Interface to get all injectable entities. Note that some bootstrappers should simply return 27 | * an empty array when their entities are not injectable. See [[ModelBootstrapper.getInjectableEntities]] 28 | * for an example of this. 29 | */ 30 | public abstract getInjectableEntities(): RegistryEntityStatic[]; 31 | 32 | /** 33 | * Kick off the bootstrapping function. The logger instance is assigned here as the injector is 34 | * not available at constructor time. 35 | * @returns {void|Promise} 36 | */ 37 | public invokeBootstrap(): void | Promise { 38 | this.logger = this.injector.get(Logger) 39 | .source(this.constructor.name); 40 | return this.bootstrap(); 41 | } 42 | 43 | /** 44 | * Assign the current Injector to the class 45 | * @param injector 46 | * @returns {EntityBootstrapper} 47 | */ 48 | public setInjector(injector: ReflectiveInjector): this { 49 | this.injector = injector; 50 | return this; 51 | } 52 | 53 | /** 54 | * Resolve & retrieve an instance of the entity from the injector. Note that depending on the 55 | * provider definition, this could be a different class to the passed token 56 | * @param token 57 | * @returns {T} 58 | */ 59 | protected getInstance(token: RegistryEntityStatic): T { 60 | const instance: T = this.injector.get(token); 61 | 62 | this.logger.info(`Resolved ${instance.constructor.name}`); 63 | 64 | return instance; 65 | } 66 | 67 | /** 68 | * Run the bootstrap method. If the implementation returns a promise, bootstrapping is halted 69 | * until the promise resolves. If promise is rejected, bootstrapping is aborted. 70 | */ 71 | protected abstract bootstrap(): void | Promise; 72 | 73 | /** 74 | * Retrieve all entities of the given type, converted from Map to Array 75 | * @param type 76 | * @returns {any[]} 77 | */ 78 | protected getFromRegistry(type: EntityType): RegistryEntityStatic[] { 79 | return [...EntityRegistry.root.getAllOfType(type).values()]; 80 | } 81 | 82 | /** 83 | * Retrieve the entities from the registry. 84 | * 85 | * @param type 86 | * @returns {RegistryEntityStatic[]} 87 | */ 88 | protected getEntitiesFromRegistry(type: EntityType): RegistryEntityStatic[] { 89 | 90 | // Note the apparent redundancy in assigning `this.entities` is intentional as the `entities` 91 | // property is used in the `getInjectableEntities` method of some implementations 92 | this.entities = this.getFromRegistry(type); 93 | return this.entities; 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/server/bootstrap/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module server 3 | */ 4 | /** 5 | * Barrel module only for exporting public implementations of bootstrap interfaces and functions 6 | */ 7 | export * from './bootstrap'; 8 | -------------------------------------------------------------------------------- /src/server/bootstrap/migrations.bootstrapper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module server 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { EntityBootstrapper } from './entity.bootstrapper'; 6 | import { AbstractMigration } from '../migrations/index'; 7 | import { RegistryEntityStatic, EntityMetadata } from '../../common/registry/entityRegistry'; 8 | 9 | /** 10 | * Provides bootstrapping of the @[[Migration]] entities 11 | */ 12 | export class MigrationBootstrapper extends EntityBootstrapper { 13 | 14 | /** 15 | * Returns all migrations registered to the [[EntityRegistry]] 16 | */ 17 | public getInjectableEntities(): RegistryEntityStatic[] { 18 | return this.getEntitiesFromRegistry('migration'); 19 | } 20 | 21 | /** 22 | * Runs all migrations, awaiting completion of all before the main bootstrapper proceed 23 | * @todo update to assign tasks to the remote cli so it doesn't happen on startup 24 | * @returns {Promise|Promise} 25 | */ 26 | public bootstrap(): Promise { 27 | 28 | this.logger.debug(`Running [${this.entities.length}] migrations`); 29 | 30 | const allMigrationPromises = this.entities.map((resolvedMigration: RegistryEntityStatic) => { 31 | 32 | this.logger.info(`migrating ${resolvedMigration.constructor.name}`); 33 | return this.getInstance(resolvedMigration) 34 | .migrate() 35 | .catch((error) => { 36 | if (error.code === 'ECONNREFUSED'){ 37 | this.logger.notice('Database not available, migration cannot run'); 38 | return; 39 | } 40 | throw error; 41 | }); 42 | 43 | }, []); 44 | 45 | return Promise.all(allMigrationPromises); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/server/bootstrap/models.bootstrapper.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { Injectable } from '@angular/core'; 3 | import { RemoteCliMock } from '../services/remoteCli.service.mock'; 4 | import { RemoteCli } from '../services/remoteCli.service'; 5 | import { ServerMock } from '../servers/abstract.server.mock'; 6 | import { Logger } from '../../common/services/logger.service'; 7 | import { LoggerMock } from '../../common/services/logger.service.mock'; 8 | import { Server } from '../servers/abstract.server'; 9 | import { bootstrap, BootstrapResponse } from './bootstrap'; 10 | import { EntityRegistry } from '../../common/registry/entityRegistry'; 11 | import { AbstractModel } from '../../common/models/model'; 12 | import { Primary } from '../../common/models/types/primary.decorator'; 13 | import { StoredProperty } from '../../common/models/types/storedProperty.decorator'; 14 | import * as typeorm from 'typeorm'; 15 | import { CreatedDate, UpdatedDate } from '../../common/models/types/timestamp.decorator'; 16 | import { AuthServiceMock } from '../services/auth.service.mock'; 17 | import { AuthService } from '../services/auth.service'; 18 | import Spy = jasmine.Spy; 19 | 20 | const providers: any[] = [ 21 | {provide: Logger, useClass: LoggerMock}, 22 | {provide: Server, useClass: ServerMock}, 23 | {provide: RemoteCli, useClass: RemoteCliMock}, 24 | {provide: AuthService, useClass: AuthServiceMock}, 25 | ]; 26 | 27 | @Injectable() 28 | class TestModel extends AbstractModel { 29 | 30 | @Primary({type: 'integer'}) 31 | public id: number; 32 | 33 | @StoredProperty({length: '10'}) 34 | public name: string; 35 | 36 | @CreatedDate() 37 | public createdAt: Date; 38 | @UpdatedDate() 39 | public updatedAt: Date; 40 | 41 | } 42 | 43 | describe('Model Bootstrapper', () => { 44 | 45 | beforeEach(() => { 46 | TestBed.configureTestingModule({ providers }); 47 | EntityRegistry.clearAll(); 48 | 49 | EntityRegistry.register('model', TestModel); 50 | 51 | }); 52 | 53 | it('resolves and initializes model with Typeorm decorators', (done: Function) => { 54 | 55 | const result = bootstrap(undefined, providers)(); 56 | 57 | const decoratorSpy = (lib: Object, decorator: string): {invoked: Spy, registered: Spy} => { 58 | const invoked = jasmine.createSpy(decorator); 59 | const registered = spyOn(lib, decorator) 60 | .and 61 | .callFake(() => invoked); 62 | return {invoked, registered}; 63 | }; 64 | 65 | const primaryColumnSpy = decoratorSpy(typeorm, 'PrimaryColumn'); 66 | const columnSpy = decoratorSpy(typeorm, 'Column'); 67 | const createDateSpy = decoratorSpy(typeorm, 'CreateDateColumn'); 68 | const updateDateSpy = decoratorSpy(typeorm, 'UpdateDateColumn'); 69 | 70 | return result.then((res: BootstrapResponse) => { 71 | 72 | expect(primaryColumnSpy.registered) 73 | .toHaveBeenCalledWith({type: 'integer'}); 74 | expect(primaryColumnSpy.invoked) 75 | .toHaveBeenCalledWith(TestModel.prototype, 'id'); 76 | 77 | expect(columnSpy.registered) 78 | .toHaveBeenCalledWith({length: '10'}); 79 | expect(columnSpy.invoked) 80 | .toHaveBeenCalledWith(TestModel.prototype, 'name'); 81 | 82 | expect(createDateSpy.registered) 83 | .toHaveBeenCalled(); 84 | expect(createDateSpy.invoked) 85 | .toHaveBeenCalledWith(TestModel.prototype, 'createdAt'); 86 | expect(updateDateSpy.registered) 87 | .toHaveBeenCalled(); 88 | expect(updateDateSpy.invoked) 89 | .toHaveBeenCalledWith(TestModel.prototype, 'updatedAt'); 90 | 91 | done(); 92 | 93 | }); 94 | 95 | }); 96 | 97 | }); 98 | -------------------------------------------------------------------------------- /src/server/bootstrap/models.bootstrapper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module server 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { EntityBootstrapper } from './entity.bootstrapper'; 6 | import { ModelStatic } from '../../common/models/model'; 7 | import { Table } from 'typeorm'; 8 | import { PrimaryColumn, Column, UpdateDateColumn, CreateDateColumn } from 'typeorm'; 9 | import { ModelMetadata } from '../../common/metadata/metadata'; 10 | 11 | /** 12 | * Provides bootstrapping of the @[[Model]] entities 13 | */ 14 | export class ModelBootstrapper extends EntityBootstrapper { 15 | 16 | /** 17 | * Models are not injectable, so this method simply returns an empty array 18 | * @returns {Array} 19 | */ 20 | public getInjectableEntities(): any[] { 21 | return []; 22 | } 23 | 24 | /** 25 | * Bootstraps the models. Each model has it's metadata mapped to the decorators from TypeORM 26 | * which are invoked to register their metadata with the internal TypeORM. This registry is later 27 | * used by the ModelStores which manipulate the TypeORM repository 28 | */ 29 | public bootstrap(): void { 30 | this.getFromRegistry('model') 31 | .forEach((model: ModelStatic) => { 32 | const meta: ModelMetadata = model.getMetadata(); 33 | 34 | this.logger.info(`initializing ${model.name}`, meta); 35 | 36 | Table(meta.storageKey, meta.tableOptions)(model); 37 | 38 | for (const [property, definition] of meta.storedProperties) { 39 | if (property === meta.identifierKey) { 40 | PrimaryColumn(definition.columnOptions)(model.prototype, property); 41 | } else { 42 | Column(definition.columnOptions)(model.prototype, property); 43 | } 44 | } 45 | 46 | if (meta.timestamps) { 47 | 48 | if (meta.timestamps.updated) { 49 | UpdateDateColumn()(model.prototype, meta.timestamps.updated); 50 | } 51 | 52 | if (meta.timestamps.created) { 53 | CreateDateColumn()(model.prototype, meta.timestamps.created); 54 | } 55 | 56 | } 57 | 58 | }); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/server/bootstrap/seeders.bootstrapper.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { Injectable } from '@angular/core'; 3 | import { RemoteCliMock } from '../services/remoteCli.service.mock'; 4 | import { RemoteCli } from '../services/remoteCli.service'; 5 | import { ServerMock } from '../servers/abstract.server.mock'; 6 | import { Logger } from '../../common/services/logger.service'; 7 | import { LoggerMock } from '../../common/services/logger.service.mock'; 8 | import { Server } from '../servers/abstract.server'; 9 | import { bootstrap, BootstrapResponse } from './bootstrap'; 10 | import { EntityRegistry } from '../../common/registry/entityRegistry'; 11 | import { AbstractSeeder } from '../seeders/index'; 12 | import Spy = jasmine.Spy; 13 | import { AuthServiceMock } from '../services/auth.service.mock'; 14 | import { AuthService } from '../services/auth.service'; 15 | 16 | let loggerInstance: Logger = new LoggerMock(); 17 | 18 | const providers: any[] = [ 19 | { 20 | provide: Logger, 21 | deps: [], 22 | useValue: loggerInstance 23 | }, 24 | {provide: Server, useClass: ServerMock}, 25 | {provide: RemoteCli, useClass: RemoteCliMock}, 26 | {provide: AuthService, useClass: AuthServiceMock}, 27 | ]; 28 | 29 | @Injectable() 30 | export class TestSeeder extends AbstractSeeder { 31 | 32 | constructor(logger: Logger) { 33 | super(logger); 34 | } 35 | 36 | public seed(): Promise { 37 | this.logger.debug('Test seeder running'); 38 | return Promise.resolve(); 39 | } 40 | 41 | } 42 | 43 | describe('Seeder Bootstrapper', () => { 44 | 45 | beforeEach(() => { 46 | TestBed.configureTestingModule({ providers }); 47 | EntityRegistry.clearAll(); 48 | 49 | EntityRegistry.register('seeder', TestSeeder); 50 | 51 | }); 52 | 53 | it('resolves and runs seeder', (done: Function) => { 54 | 55 | const loggerSpy = spyOn(loggerInstance, 'persistLog') 56 | .and 57 | .callThrough(); 58 | spyOn(loggerInstance, 'source') 59 | .and 60 | .callFake(() => loggerInstance); 61 | 62 | const result = bootstrap(undefined, providers)(); 63 | 64 | return result.then((res: BootstrapResponse) => { 65 | 66 | expect(loggerSpy) 67 | .toHaveBeenCalledWith('debug', ['Test seeder running']); 68 | done(); 69 | 70 | }); 71 | 72 | }); 73 | 74 | }); 75 | -------------------------------------------------------------------------------- /src/server/bootstrap/seeders.bootstrapper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module server 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { EntityBootstrapper } from './entity.bootstrapper'; 6 | import { AbstractSeeder } from '../seeders/index'; 7 | import { RegistryEntityStatic, EntityMetadata } from '../../common/registry/entityRegistry'; 8 | 9 | /** 10 | * Provides bootstrapping of the @[[Seeder]] entities 11 | */ 12 | export class SeederBootstrapper extends EntityBootstrapper { 13 | 14 | /** 15 | * Returns all seeders registered to the [[EntityRegistry]] 16 | */ 17 | public getInjectableEntities(): RegistryEntityStatic[] { 18 | return this.getEntitiesFromRegistry('seeder'); 19 | } 20 | 21 | /** 22 | * Runs all seeders, awaiting completion of all before the main bootstrapper proceed 23 | * @todo update to assign tasks to the remote cli so it doesn't happen on startup 24 | * @returns {Promise|Promise} 25 | */ 26 | public bootstrap(): Promise { 27 | const allSeederPromises = this.entities.map((resolvedSeeder: RegistryEntityStatic) => { 28 | 29 | return this.getInstance(resolvedSeeder).seed(); 30 | 31 | }, []); 32 | 33 | return Promise.all(allSeederPromises); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/server/bootstrap/services.bootstrapper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module server 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { EntityBootstrapper } from './entity.bootstrapper'; 6 | import { AbstractService } from '../../common/services/service'; 7 | import { RegistryEntityStatic, EntityMetadata } from '../../common/registry/entityRegistry'; 8 | 9 | /** 10 | * Provides bootstrapping of the @[[Service]] entities 11 | */ 12 | export class ServiceBootstrapper extends EntityBootstrapper { 13 | 14 | /** 15 | * Returns all services registered to the [[EntityRegistry]] 16 | */ 17 | public getInjectableEntities(): RegistryEntityStatic[] { 18 | return this.getEntitiesFromRegistry('service'); 19 | } 20 | 21 | /** 22 | * Bootstrap all services. With each service, the initialize function is invoked, and the bootstrap 23 | * awaits all services to complete initializing before resolving the promise 24 | * @returns {Promise|Promise} 25 | */ 26 | public bootstrap(): Promise { 27 | 28 | this.logger.debug(`Initializing [${this.entities.length}] services`); 29 | 30 | const allServicePromises = this.entities.map((resolvedService: RegistryEntityStatic) => { 31 | 32 | let service = this.getInstance(resolvedService); 33 | 34 | return Promise.resolve(service.initialize()); 35 | }, []); 36 | 37 | return Promise.all(allServicePromises); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/server/controllers/abstract.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { inject, TestBed, async } from '@angular/core/testing'; 2 | import { Injectable } from '@angular/core'; 3 | import { Logger } from '../../common/services/logger.service'; 4 | import { Server, RouteConfig } from '../servers/abstract.server'; 5 | import { LoggerMock } from '../../common/services/logger.service.mock'; 6 | import { ServerMock } from '../servers/abstract.server.mock'; 7 | import { RemoteCli } from '../services/remoteCli.service'; 8 | import { RemoteCliMock } from '../services/remoteCli.service.mock'; 9 | import { Request } from './request'; 10 | import { Response } from './response'; 11 | import { AbstractController } from './abstract.controller'; 12 | import { Route } from './route.decorator'; 13 | import { UnavailableForLegalReasonsException } from '../../common/exceptions/exceptions'; 14 | import { AuthServiceMock } from '../services/auth.service.mock'; 15 | import { AuthService } from '../services/auth.service'; 16 | 17 | @Injectable() 18 | class TestController extends AbstractController { 19 | 20 | constructor(logger: Logger) { 21 | super(logger); 22 | } 23 | 24 | @Route('GET', '/test') 25 | public test(request: Request, response: Response): Response { 26 | return response.data('Hello World'); 27 | } 28 | 29 | @Route('GET', '/http-error') 30 | public httpError(request: Request, response: Response): Response { 31 | throw new UnavailableForLegalReasonsException("You can't see that"); 32 | } 33 | 34 | @Route('GET', '/unknown-error') 35 | public unknownError(request: Request, response: Response): Response { 36 | throw new Error('Something went terribly wrong'); 37 | } 38 | 39 | } 40 | 41 | const providers = [ 42 | TestController, 43 | {provide: Logger, useClass: LoggerMock}, 44 | {provide: Server, useClass: ServerMock}, 45 | {provide: RemoteCli, useClass: RemoteCliMock}, 46 | {provide: AuthService, useClass: AuthServiceMock}, 47 | ]; 48 | 49 | describe('Controller', () => { 50 | 51 | beforeEach(() => { 52 | TestBed.configureTestingModule({ providers }); 53 | }); 54 | 55 | it('Registers a route that returns a response', async(inject([TestController, Server], 56 | (c: TestController, s: Server) => { 57 | 58 | c.registerRoutes(s); 59 | 60 | const callStackHandler = s.configuredRoutes.find((r: RouteConfig) => r.methodName == 'test').callStackHandler; 61 | 62 | let request = new Request(); 63 | let response = new Response(); 64 | 65 | return callStackHandler(request, response) 66 | .then((finalResponse) => { 67 | 68 | expect(finalResponse.body) 69 | .toEqual('Hello World'); 70 | 71 | }); 72 | 73 | }))); 74 | 75 | it('Registers a route that returns an http error response', async(inject([TestController, Server], 76 | (c: TestController, s: Server) => { 77 | 78 | c.registerRoutes(s); 79 | 80 | const callStackHandler = s.configuredRoutes.find((r: RouteConfig) => r.methodName == 'httpError').callStackHandler; 81 | 82 | let request = new Request(); 83 | let response = new Response(); 84 | 85 | return callStackHandler(request, response) 86 | .then((finalResponse: Response) => { 87 | 88 | expect(finalResponse.statusCode) 89 | .toEqual(451); 90 | 91 | expect(finalResponse.body) 92 | .toEqual({message: "UnavailableForLegalReasonsException: You can't see that"}); 93 | 94 | }); 95 | 96 | }))); 97 | 98 | it('Registers a route that falls back to an http error respnse', async(inject([TestController, Server], 99 | (c: TestController, s: Server) => { 100 | 101 | c.registerRoutes(s); 102 | 103 | const callStackHandler = s.configuredRoutes.find((r: RouteConfig) => r.methodName == 'unknownError').callStackHandler; 104 | 105 | let request = new Request(); 106 | let response = new Response(); 107 | 108 | return callStackHandler(request, response) 109 | .then((finalResponse: Response) => { 110 | 111 | expect(finalResponse.statusCode) 112 | .toEqual(500); 113 | 114 | expect(finalResponse.body) 115 | .toEqual({message: 'InternalServerErrorException: Something went terribly wrong'}); 116 | 117 | }); 118 | 119 | }))); 120 | 121 | }); 122 | -------------------------------------------------------------------------------- /src/server/controllers/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module server 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | export * from './abstract.controller'; 6 | export * from './resource.controller'; 7 | export * from './route.decorator'; 8 | export * from './request' 9 | export * from './response' 10 | -------------------------------------------------------------------------------- /src/server/controllers/request.spec.ts: -------------------------------------------------------------------------------- 1 | import { Request } from '../controllers/request'; 2 | import { IncomingMessage } from 'http'; 3 | import { EventEmitter } from 'events'; 4 | import { 5 | UnprocessableEntityException, 6 | PayloadTooLargeException 7 | } from '../../common/exceptions/exceptions'; 8 | 9 | describe('Request', () => { 10 | 11 | it('Returns promise of the payload data', (done: Function) => { 12 | const payload = {message: 'hello there'}; 13 | 14 | let emitter = new EventEmitter(); 15 | 16 | (emitter as any).setEncoding = (): any => null; 17 | 18 | let request = new Request(emitter as IncomingMessage); 19 | 20 | request.getPayload() 21 | .then((result) => { 22 | expect(result) 23 | .toEqual(payload); 24 | done(); 25 | }); 26 | 27 | process.nextTick(() => { 28 | emitter.emit(Request.EVENT_DATA, JSON.stringify(payload)); 29 | emitter.emit(Request.EVENT_END); 30 | }); 31 | 32 | }); 33 | 34 | it('Throws exception when payload is not json', (done: Function) => { 35 | 36 | let emitter = new EventEmitter(); 37 | 38 | (emitter as IncomingMessage).setEncoding = (): any => null; 39 | 40 | let request = new Request(emitter as IncomingMessage); 41 | 42 | request.getPayload() 43 | .catch((error) => { 44 | expect(error instanceof UnprocessableEntityException) 45 | .toBe(true); 46 | done(); 47 | }); 48 | 49 | process.nextTick(() => { 50 | emitter.emit(Request.EVENT_DATA, 'definitely_not_json'); 51 | emitter.emit(Request.EVENT_END); 52 | }); 53 | }); 54 | 55 | it('Throws exception when payload is too large', (done: Function) => { 56 | 57 | let emitter = new EventEmitter(); 58 | 59 | (emitter as IncomingMessage).setEncoding = (): any => null; 60 | 61 | const destroySpy = jasmine.createSpy('socket_destroy'); 62 | 63 | (emitter as any).socket = { 64 | destroy: destroySpy 65 | }; 66 | 67 | let request = new Request(emitter as IncomingMessage); 68 | 69 | request.getPayload() 70 | .catch((error) => { 71 | expect(error instanceof PayloadTooLargeException) 72 | .toBe(true); 73 | done(); 74 | expect(destroySpy) 75 | .toHaveBeenCalled(); 76 | }); 77 | 78 | process.nextTick(() => { 79 | emitter.emit(Request.EVENT_DATA, 'a'.repeat(1e6 + 1)); //this may be a fairly intensive test, not sure 80 | // about the impact 81 | }); 82 | 83 | }); 84 | 85 | it('has utility function for converting flat dictionary to map', () => { 86 | 87 | const dict = { 88 | a: 1, 89 | b: 2 90 | }; 91 | 92 | const map = Request.extractMapFromDictionary(dict); 93 | 94 | expect(map.get('a')) 95 | .toEqual(1); 96 | expect(map.get('b')) 97 | .toEqual(2); 98 | 99 | }); 100 | 101 | it('can get the raw event emitter', () => { 102 | 103 | let emitter = new EventEmitter(); 104 | 105 | let request = new Request(emitter as IncomingMessage); 106 | 107 | expect(request.getRaw()) 108 | .toEqual(emitter); 109 | 110 | }); 111 | 112 | it('can get the params map', () => { 113 | let request = new Request(null, new Map([['id', '1']])); 114 | expect(request.params() 115 | .get('id')) 116 | .toEqual('1'); 117 | }); 118 | 119 | it('can get the headers map', () => { 120 | let request = new Request(null, null, new Map([['Authorization', 'Basic: something']])); 121 | expect(request.headers() 122 | .get('Authorization')) 123 | .toEqual('Basic: something'); 124 | }); 125 | 126 | }); 127 | -------------------------------------------------------------------------------- /src/server/controllers/request.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module server 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { IncomingMessage } from 'http'; 6 | import { 7 | PayloadTooLargeException, 8 | UnprocessableEntityException 9 | } from '../../common/exceptions/exceptions'; 10 | 11 | /** 12 | * Request class that is passed into all middleware and controller methods to extract data from the 13 | * client request. 14 | */ 15 | export class Request { 16 | 17 | /** @event Emitted when request body data is read */ 18 | public static EVENT_DATA: string = 'data'; 19 | /** @event Emitted when request body read is completed */ 20 | public static EVENT_END: string = 'end'; 21 | /** @event Emitted when response body read is completed */ 22 | public static EVENT_ERROR: string = 'end'; 23 | 24 | constructor(protected raw: IncomingMessage = undefined, 25 | protected paramsMap: Map = new Map(), 26 | protected headersMap: Map = new Map()) { 27 | 28 | } 29 | 30 | /** 31 | * Get the raw IncomingMessage 32 | * @returns {IncomingMessage} 33 | */ 34 | public getRaw(): IncomingMessage { 35 | return this.raw; 36 | } 37 | 38 | /** 39 | * Get all headers as a Map 40 | * @returns {Map} 41 | */ 42 | public headers(): Map { 43 | return this.headersMap; 44 | } 45 | 46 | /** 47 | * Get all route paramaters as a Map 48 | * @returns {Map} 49 | */ 50 | public params(): Map { 51 | return this.paramsMap; 52 | } 53 | 54 | /** 55 | * Helper function for converting Dictionary to Map 56 | * @param dictionary 57 | * @returns {Map} 58 | */ 59 | public static extractMapFromDictionary(dictionary: Object): Map { 60 | let map = new Map(); 61 | 62 | for (let key in dictionary) { 63 | map.set(key, dictionary[key]); 64 | } 65 | 66 | return map; 67 | } 68 | 69 | /** 70 | * Extract the passed body object from the raw request 71 | * @returns {Promise} 72 | */ 73 | public getPayload(): Promise { 74 | return new Promise((resolve, reject) => { 75 | this.raw.setEncoding('utf8'); 76 | 77 | let data: string = ''; 78 | 79 | this.raw.on(Request.EVENT_DATA, (d: string) => { 80 | data += d; 81 | if (data.length > 1e6) { 82 | data = ""; 83 | let e = new PayloadTooLargeException(); 84 | this.raw.socket.destroy(); 85 | reject(e); 86 | } 87 | }); 88 | 89 | this.raw.on(Request.EVENT_END, () => { 90 | try { 91 | resolve(JSON.parse(data)); 92 | } catch (e) { 93 | reject(new UnprocessableEntityException()); 94 | } 95 | 96 | }); 97 | 98 | this.raw.on(Request.EVENT_ERROR, reject); 99 | }); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/server/controllers/resource.controller.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module server 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { Injectable } from '@angular/core'; 6 | import { AbstractController } from './abstract.controller'; 7 | import { Logger } from '../../common/services/logger.service'; 8 | import { Route } from './route.decorator'; 9 | import { AbstractModel } from '../../common/models/model'; 10 | import { Response } from './response'; 11 | import { Request } from './request'; 12 | import { AbstractStore } from '../../common/stores/store'; 13 | import { Collection } from '../../common/models/collection'; 14 | import { ValidatorOptions } from '../../common/validation'; 15 | import { NotFoundException } from '../../common/exceptions/exceptions'; 16 | 17 | /** 18 | * Provides resource controller that all controllers that interact RESTfully with ModelStores 19 | * should extend from. 20 | */ 21 | @Injectable() 22 | export abstract class ResourceController extends AbstractController { 23 | 24 | constructor(logger: Logger, protected modelStore: AbstractStore) { 25 | super(logger); 26 | } 27 | 28 | /** 29 | * Get one entity 30 | * @param request 31 | * @param response 32 | * @returns {any} 33 | */ 34 | @Route('GET', '/:id') 35 | public async getOne(request: Request, response: Response): Promise { 36 | const model: M = await this.modelStore.findOne(request.params().get('id')); 37 | return response.data(model); 38 | } 39 | 40 | /** 41 | * Get many entities 42 | * @param request 43 | * @param response 44 | * @returns {any} 45 | */ 46 | @Route('GET', '/') 47 | public async getMany(request: Request, response: Response): Promise { 48 | const collection: Collection = await this.modelStore.findMany(); 49 | return response.data(collection); 50 | } 51 | 52 | /** 53 | * Process and persist an entity 54 | * @param request 55 | * @param response 56 | */ 57 | @Route('PUT', '/:id') 58 | public putOne(request: Request, response: Response): Promise { 59 | 60 | return this.savePayload(request, response); 61 | } 62 | 63 | /** 64 | * Process and update entity, skipping validation of any missing properties 65 | * @param request 66 | * @param response 67 | * @returns {Promise} 68 | */ 69 | @Route('PATCH', '/:id') 70 | public patchOne(request: Request, response: Response): Promise { 71 | return this.savePayload(request, response, true, { 72 | skipMissingProperties: true, 73 | }); 74 | } 75 | 76 | /** 77 | * Delete the payload model from the model store 78 | * @param request 79 | * @param response 80 | * @returns {Promise} 81 | */ 82 | @Route('DELETE', '/:id') 83 | public async deleteOne(request: Request, response: Response): Promise { 84 | 85 | const model: M = await this.modelStore.hydrate(await request.getPayload()); 86 | await this.modelStore.deleteOne(model); 87 | return response.data(model); 88 | } 89 | 90 | /** 91 | * Persist the request payload with the model store with optional validator options 92 | * @param request 93 | * @param response 94 | * @param validatorOptions 95 | * @param checkExists 96 | * @returns {Promise} 97 | */ 98 | protected async savePayload( 99 | request: Request, 100 | response: Response, 101 | checkExists: boolean = false, 102 | validatorOptions: ValidatorOptions = {}, 103 | ): Promise | never { 104 | 105 | const model = await this.modelStore.hydrate(await request.getPayload()); 106 | 107 | if (checkExists) { 108 | const exists = await this.modelStore.hasOne(model); 109 | if (!exists) { 110 | throw new NotFoundException(`Model with id [${model.getIdentifier()}] does not exist`); 111 | } 112 | } 113 | 114 | await this.modelStore.validate(model, validatorOptions); 115 | await this.modelStore.saveOne(model); 116 | return response.data(model); 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/server/controllers/response.spec.ts: -------------------------------------------------------------------------------- 1 | import { Response } from './response'; 2 | 3 | describe('Response', () => { 4 | 5 | it('stores the data and emits an event', (done: Function) => { 6 | 7 | const response = new Response(); 8 | const dataFixture = {a: 1}; 9 | 10 | let emittedData: any; 11 | response.on(Response.EVENT_DATA, (data: any) => { 12 | emittedData = data; 13 | }); 14 | 15 | response.on(Response.EVENT_END, () => { 16 | expect(emittedData) 17 | .toEqual(dataFixture); 18 | done(); 19 | }); 20 | 21 | response.data(dataFixture); 22 | 23 | expect(response.body) 24 | .toEqual(dataFixture); 25 | 26 | }); 27 | 28 | it('stores new headers', () => { 29 | 30 | const response = new Response(); 31 | response.header('Foo', 'Bar'); 32 | 33 | expect(response.headers.get('Foo')) 34 | .toEqual('Bar'); 35 | 36 | }); 37 | 38 | it('sets the status code', () => { 39 | 40 | const response = new Response(); 41 | response.status(321); 42 | 43 | expect(response.statusCode) 44 | .toEqual(321); 45 | 46 | }); 47 | 48 | it('has convenience function for created resource', () => { 49 | 50 | const response = new Response(); 51 | const fixture = {a: 1}; 52 | const res = response.created(fixture); 53 | 54 | expect(response.statusCode) 55 | .toEqual(201); 56 | expect(response.body) 57 | .toEqual(fixture); 58 | expect(res) 59 | .toEqual(response); 60 | 61 | }); 62 | 63 | }); 64 | -------------------------------------------------------------------------------- /src/server/controllers/response.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module server 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { EventEmitter } from 'events'; 6 | 7 | /** 8 | * Response class that is passed into all middleware and controller methods to be manipulated 9 | * and eventually dispatched to the client. 10 | * 11 | * Response is also an `EventEmitter` so you can attach listeners to the response: 12 | * * [[Response.EVENT_DATA]] 13 | * * [[Response.EVENT_END]] 14 | * 15 | */ 16 | export class Response extends EventEmitter { 17 | 18 | /** @event Emitted when response body data is set */ 19 | public static EVENT_DATA:string = 'data'; 20 | /** @event Emitted when response body setting is completed */ 21 | public static EVENT_END:string = 'end'; 22 | 23 | /** The HTTP status code that is sent */ 24 | public statusCode: number = 200; 25 | /** Custom message (if any) associated with custom status codes */ 26 | public statusMessage: string; 27 | /** Map of headers to send back */ 28 | public headers: Map = new Map(); 29 | /** Body content, should almost always be JSON */ 30 | public body: any; 31 | 32 | /** 33 | * Set the data to be sent back in the body. 34 | * This also emits a `data` event containing the response body and immediately sends `end` event 35 | * @param data 36 | * @returns {Response} 37 | */ 38 | public data(data: any): this { 39 | this.body = data; 40 | this.emit(Response.EVENT_DATA, data); 41 | this.emit(Response.EVENT_END); 42 | return this; 43 | } 44 | 45 | /** 46 | * Set the status code to be sent 47 | * @param code 48 | * @returns {Response} 49 | */ 50 | public status(code: number): this { 51 | this.statusCode = code; 52 | return this; 53 | } 54 | 55 | /** 56 | * Add or overwrite a header 57 | * @param name 58 | * @param value 59 | * @returns {Response} 60 | */ 61 | public header(name: string, value: string): this { 62 | this.headers.set(name, value); 63 | return this; 64 | } 65 | 66 | /** 67 | * Utility method to send the correct status code for created entity 68 | * @param data 69 | * @returns {Response} 70 | */ 71 | public created(data: any): this { 72 | return this 73 | .status(201) 74 | .data(data); 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/server/controllers/route.decorator.spec.ts: -------------------------------------------------------------------------------- 1 | import { inject, TestBed } from '@angular/core/testing'; 2 | import { Request } from '../controllers/request'; 3 | import { Response } from '../controllers/response'; 4 | import { AbstractController } from '../controllers/abstract.controller'; 5 | import { Injectable, Injector } from '@angular/core'; 6 | import { Logger } from '../../common/services/logger.service'; 7 | import { Server, RouteConfig } from '../servers/abstract.server'; 8 | import { LoggerMock } from '../../common/services/logger.service.mock'; 9 | import { ServerMock } from '../servers/abstract.server.mock'; 10 | import { RemoteCli } from '../services/remoteCli.service'; 11 | import { RemoteCliMock } from '../services/remoteCli.service.mock'; 12 | import { Route } from './route.decorator'; 13 | import { AuthServiceMock } from '../services/auth.service.mock'; 14 | import { AuthService } from '../services/auth.service'; 15 | import { Controller } from '../registry/decorators'; 16 | 17 | @Injectable() 18 | @Controller({ 19 | routeBase: 'base' 20 | }) 21 | class TestController extends AbstractController { 22 | 23 | constructor(logger: Logger) { 24 | super(logger); 25 | } 26 | 27 | @Route('PUT', '/test/:id') 28 | public testMethod(request: Request, response: Response): any { 29 | } 30 | 31 | } 32 | 33 | const providers = [ 34 | TestController, 35 | {provide: Server, useClass: ServerMock}, 36 | {provide: Logger, useClass: LoggerMock}, 37 | {provide: RemoteCli, useClass: RemoteCliMock}, 38 | {provide: AuthService, useClass: AuthServiceMock}, 39 | ]; 40 | 41 | describe('@Route decorator', () => { 42 | 43 | beforeEach(() => { 44 | TestBed.configureTestingModule({ providers }); 45 | }); 46 | 47 | it('Registers a route definition with the server ', 48 | inject([TestController, Injector, Server], 49 | (c: TestController, i: Injector, s: Server) => { 50 | 51 | let controller = c.registerInjector(i) 52 | .registerRoutes(s); 53 | 54 | const routeConfig: RouteConfig = s.getRoutes() 55 | .find((route: RouteConfig) => route.methodName == 'testMethod'); 56 | 57 | expect(routeConfig.method) 58 | .toEqual('PUT'); 59 | expect(routeConfig.path) 60 | .toEqual(process.env.API_BASE + '/base/test/:id'); 61 | 62 | })); 63 | 64 | }); 65 | -------------------------------------------------------------------------------- /src/server/controllers/route.decorator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module server 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { HttpMethod } from '../servers/abstract.server'; 6 | /** 7 | * Decorator for registering a basic action method in a controller 8 | * @param method 9 | * @param route 10 | * @returns {function(any, string, TypedPropertyDescriptor): void} 11 | * @constructor 12 | */ 13 | export function Route(method: HttpMethod, route: string): MethodDecorator { 14 | 15 | return function (target: any, propertyKey: string, descriptor: TypedPropertyDescriptor): void { 16 | 17 | target.registerActionMethod(propertyKey, method, route); 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/server/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the server-only module. It exports classes, functions and interfaces that are for the 3 | * server implementations to use 4 | * 5 | * Any of the types (classes, functions etc) defined under this module can be imported from 6 | * `@zerothstack/core/server` 7 | * 8 | * Example: 9 | * ```typescript 10 | * import { AbstractController } from '@zerothstack/core/server'; 11 | * ``` 12 | * 13 | * @module server 14 | * @preferred 15 | */ 16 | /** */ 17 | export * from './main'; 18 | export * from './bootstrap'; 19 | export * from './controllers'; 20 | export * from './servers'; 21 | export * from './services'; 22 | export * from './seeders'; 23 | export * from './migrations'; 24 | export * from './stores'; 25 | export * from './middleware'; 26 | export * from './registry'; 27 | -------------------------------------------------------------------------------- /src/server/main.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module server 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { Server } from './servers/abstract.server'; 6 | import { Database } from './services/database.service'; 7 | import { RemoteCli } from './services/remoteCli.service'; 8 | import { Logger } from '../common/services/logger.service'; 9 | import { ConsoleLogger } from '../common/services/consoleLogger.service'; 10 | import { DebugLogMiddleware } from './middleware/debugLog.middleware'; 11 | import { ExpressServer } from './servers/express.server'; 12 | import * as dotenv from 'dotenv'; 13 | import * as path from 'path'; 14 | import * as _ from 'lodash'; 15 | // import { ProviderDefinition } from './bootstrap/bootstrap'; 16 | // import { platformServer, ServerModule } from '@angular/platform-server'; 17 | // import { NgModule } from '@angular/core'; 18 | import { ProviderDefinition } from './bootstrap/bootstrap'; 19 | 20 | // Load .env variables into process.env.* 21 | dotenv.config({ 22 | path: path.resolve(process.cwd(), '.env') 23 | }); 24 | 25 | // Strip the prefix PUBLIC_ from any exported .env vars 26 | process.env = _.mapKeys(process.env, (value: any, key: string) => key.replace(/^PUBLIC_/, '')); 27 | 28 | /** 29 | * Core providers, can be overridden by implementing app with it's own provider definitions 30 | * @type {ProviderDefinition[]} 31 | */ 32 | export const CORE_PROVIDERS: ProviderDefinition[] = [ 33 | Database, 34 | RemoteCli, 35 | DebugLogMiddleware, 36 | // {provide: Server, useClass: HapiServer}, 37 | {provide: Server, useClass: ExpressServer}, 38 | {provide: Logger, useClass: ConsoleLogger}, 39 | ]; 40 | -------------------------------------------------------------------------------- /src/server/middleware/debugLog.middleware.spec.ts: -------------------------------------------------------------------------------- 1 | import { inject, TestBed, async } from '@angular/core/testing'; 2 | import { Request } from '../controllers/request'; 3 | import { Response } from '../controllers/response'; 4 | import { Route } from '../controllers/route.decorator'; 5 | import { Before } from './middleware.decorator'; 6 | import { AbstractController } from '../controllers/abstract.controller'; 7 | import { Injectable, Injector } from '@angular/core'; 8 | import { Logger } from '../../common/services/logger.service'; 9 | import { Server, RouteConfig } from '../servers/abstract.server'; 10 | import { LoggerMock } from '../../common/services/logger.service.mock'; 11 | import { ServerMock } from '../servers/abstract.server.mock'; 12 | import { RemoteCli } from '../services/remoteCli.service'; 13 | import { RemoteCliMock } from '../services/remoteCli.service.mock'; 14 | import { debugLog, DebugLogMiddleware } from './debugLog.middleware'; 15 | import { AuthServiceMock } from '../services/auth.service.mock'; 16 | import { AuthService } from '../services/auth.service'; 17 | 18 | @Injectable() 19 | class MiddlewareController extends AbstractController { 20 | 21 | constructor(logger: Logger) { 22 | super(logger); 23 | } 24 | 25 | @Route('GET', '/test') 26 | @Before(debugLog('test log input')) 27 | public testMethod(request: Request, response: Response): Response { 28 | return response; 29 | } 30 | 31 | } 32 | 33 | let source: string, logs: any[] = []; 34 | 35 | let mockLogger = { 36 | source: (input: string) => { 37 | source = input; 38 | return mockLogger; 39 | }, 40 | debug: (input: string) => { 41 | logs.push(input); 42 | } 43 | }; 44 | 45 | const providers: any[] = [ 46 | MiddlewareController, 47 | {provide: Server, useClass: ServerMock}, 48 | {provide: Logger, useClass: LoggerMock}, 49 | {provide: RemoteCli, useClass: RemoteCliMock}, 50 | {provide: AuthService, useClass: AuthServiceMock}, 51 | { 52 | provide: DebugLogMiddleware, 53 | deps: [], 54 | useFactory: () => { 55 | return new DebugLogMiddleware(mockLogger) 56 | }, 57 | } 58 | ] 59 | ; 60 | 61 | describe('debugLog middleware', () => { 62 | 63 | let controller: MiddlewareController; 64 | 65 | beforeEach(() => { 66 | TestBed.configureTestingModule({ providers }); 67 | }); 68 | 69 | it('Calls debug.log on the passed value to the middleware decorator', 70 | async(inject([MiddlewareController, Injector, Server], 71 | (c: MiddlewareController, i: Injector, s: Server) => { 72 | 73 | controller = c.registerInjector(i) 74 | .registerRoutes(s); 75 | 76 | const callStackHandler: any = s.getRoutes() 77 | .find((route: RouteConfig) => route.methodName == 'testMethod').callStackHandler; 78 | 79 | let request = new Request(); 80 | let response = new Response(); 81 | 82 | return callStackHandler(request, response) 83 | .then(() => { 84 | 85 | expect(source) 86 | .toEqual('debugLog'); 87 | expect(logs) 88 | .toEqual(['test log input']); 89 | 90 | }); 91 | 92 | }))); 93 | 94 | }); 95 | -------------------------------------------------------------------------------- /src/server/middleware/debugLog.middleware.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module server 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { Injectable, ReflectiveInjector } from '@angular/core'; 6 | import { Logger } from '../../common/services/logger.service'; 7 | import { InjectableMiddleware, Middleware, InjectableMiddlewareFactory } from './index'; 8 | import { Response } from '../controllers/response'; 9 | import { Request } from '../controllers/request'; 10 | 11 | /** 12 | * Basic debug logger middleware See [[debugLog]] for usage 13 | */ 14 | @Injectable() 15 | export class DebugLogMiddleware implements InjectableMiddleware { 16 | protected logger: Logger; 17 | 18 | constructor(loggerBase: Logger) { 19 | this.logger = loggerBase.source('debugLog'); 20 | } 21 | 22 | /** 23 | * Creates the debugLog middleware with binding to current class for access to logger 24 | * @param messages 25 | * @returns {any} 26 | */ 27 | public middlewareFactory(messages: string[]): Middleware { 28 | 29 | return function debugLog(request: Request, response: Response): Response { 30 | this.logger.debug(...messages); 31 | return response; 32 | }.bind(this); 33 | 34 | } 35 | } 36 | 37 | /** 38 | * Logs messages to the Logger implementation when middleware is invoked 39 | * Passes through any responses 40 | * 41 | * Example usage: 42 | * ```typescript 43 | * @Injectable() 44 | * @Controller() 45 | * class ExampleController extends AbstractController { 46 | * 47 | * constructor(server: Server, logger: Logger) { 48 | * super(server, logger); 49 | * } 50 | * 51 | * @Route('GET', '/test') 52 | * @Before(debugLog('test log input')) 53 | * public testMethod(request: Request, response: Response): Response { 54 | * return response; 55 | * } 56 | * 57 | * } 58 | * ``` 59 | * When `GET /test` is called, "test log input" will be logged before the testMethod is invoked 60 | * 61 | * @param messages 62 | * @returns {function(ReflectiveInjector): Middleware} 63 | */ 64 | export function debugLog(...messages: string[]): InjectableMiddlewareFactory { 65 | 66 | return (injector: ReflectiveInjector): Middleware => { 67 | return injector.get(DebugLogMiddleware).middlewareFactory(messages); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/server/middleware/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module server 3 | */ 4 | /** 5 | * Barrel module only for exporting middleware implementations and interfaces 6 | */ 7 | import { Injector } from '@angular/core'; 8 | import { Response } from '../controllers/response'; 9 | import { Request } from '../controllers/request'; 10 | 11 | export interface Middleware { 12 | (request: Request, response: Response): Response | Promise; 13 | } 14 | 15 | export interface InjectableMiddleware { 16 | middlewareFactory(...args: any[]): Middleware; 17 | } 18 | 19 | export interface InjectableMiddlewareFactory { 20 | (injector: Injector): Middleware; 21 | } 22 | 23 | export interface IsolatedMiddlewareFactory { 24 | (injector?: Injector): Middleware; 25 | } 26 | 27 | export type MiddlewareFactory = InjectableMiddlewareFactory | IsolatedMiddlewareFactory; 28 | 29 | export * from './middleware.decorator'; 30 | export * from './debugLog.middleware'; 31 | -------------------------------------------------------------------------------- /src/server/middleware/middleware.decorator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module server 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { 6 | AbstractController, 7 | ControllerConstructor, 8 | ControllerStatic 9 | } from '../controllers/abstract.controller'; 10 | import { MiddlewareFactory } from './index'; 11 | import { initializeMetadata } from '../../common/metadata/metadata'; 12 | import { RegistryEntityStatic } from '../../common/registry/entityRegistry'; 13 | import { MiddlewareRegistry, ControllerMetadata } from '../registry/decorators'; 14 | 15 | /** 16 | * Decorator for assigning before middleware method in a controller 17 | * @returns {function(any, string, TypedPropertyDescriptor): undefined} 18 | * @constructor 19 | * @param middlewareFactories 20 | */ 21 | export function Before(...middlewareFactories: MiddlewareFactory[]): MethodDecorator { 22 | 23 | return function (target: ControllerConstructor, propertyKey: string, descriptor: TypedPropertyDescriptor) { 24 | 25 | target.constructor.registerMiddleware('before', middlewareFactories, propertyKey); 26 | }; 27 | } 28 | 29 | /** 30 | * Decorator for assigning after middleware method in a controller 31 | * @returns {function(any, string, TypedPropertyDescriptor): undefined} 32 | * @constructor 33 | * @param middlewareFactories 34 | */ 35 | export function After(...middlewareFactories: MiddlewareFactory[]): MethodDecorator { 36 | 37 | return function (target: ControllerConstructor, propertyKey: string, descriptor: TypedPropertyDescriptor) { 38 | 39 | target.constructor.registerMiddleware('after', middlewareFactories, propertyKey); 40 | }; 41 | } 42 | 43 | /** 44 | * Initializes the `registeredMiddleware` property on the controller with empty stores 45 | * @param target 46 | */ 47 | export function initializeMiddlewareRegister(target: RegistryEntityStatic): void { 48 | initializeMetadata(target); 49 | if (!target.__metadata.middleware) { 50 | target.__metadata.middleware = { 51 | methods: new Map(), 52 | all: { 53 | before: [], 54 | after: [] 55 | } 56 | }; 57 | } 58 | } 59 | 60 | /** 61 | * Decorator for assigning before middleware to all methods in a controller 62 | * @param middlewareFactories 63 | * @returns {function(AbstractController): void} 64 | * @constructor 65 | */ 66 | export function BeforeAll(...middlewareFactories: MiddlewareFactory[]): ClassDecorator { 67 | return function (target: ControllerStatic): void { 68 | target.registerMiddleware('before', middlewareFactories); 69 | } 70 | } 71 | 72 | /** 73 | * Decorator for assigning after middleware to all methods in a controller 74 | * @param middlewareFactories 75 | * @returns {function(AbstractController): void} 76 | * @constructor 77 | */ 78 | export function AfterAll(...middlewareFactories: MiddlewareFactory[]): ClassDecorator { 79 | return function (target: ControllerStatic): void { 80 | target.registerMiddleware('after', middlewareFactories); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/server/middleware/middleware.spec.ts: -------------------------------------------------------------------------------- 1 | import { inject, TestBed, async } from '@angular/core/testing'; 2 | import { IsolatedMiddlewareFactory } from './index'; 3 | import { Request } from '../controllers/request'; 4 | import { Response } from '../controllers/response'; 5 | import { Route } from '../controllers/route.decorator'; 6 | import { AfterAll, BeforeAll, Before, After } from './middleware.decorator'; 7 | import { AbstractController } from '../controllers/abstract.controller'; 8 | import { Injectable, ReflectiveInjector } from '@angular/core'; 9 | import { Logger } from '../../common/services/logger.service'; 10 | import { Server, RouteConfig } from '../servers/abstract.server'; 11 | import { LoggerMock } from '../../common/services/logger.service.mock'; 12 | import { ServerMock } from '../servers/abstract.server.mock'; 13 | import { RemoteCli } from '../services/remoteCli.service'; 14 | import { RemoteCliMock } from '../services/remoteCli.service.mock'; 15 | import { PromiseFactory } from '../../common/util/serialPromise'; 16 | import { AuthServiceMock } from '../services/auth.service.mock'; 17 | import { AuthService } from '../services/auth.service'; 18 | 19 | let middlewareCalls: string[] = []; 20 | 21 | function middlewareFixture(input: string): IsolatedMiddlewareFactory { 22 | return () => function mockMiddleware(request: Request, response: Response): Response { 23 | middlewareCalls.push(input); 24 | return response; 25 | } 26 | } 27 | 28 | @BeforeAll(middlewareFixture('one'), middlewareFixture('two')) 29 | @AfterAll(middlewareFixture('five')) 30 | @Injectable() 31 | class MiddlewareController extends AbstractController { 32 | 33 | constructor(logger: Logger) { 34 | super(logger); 35 | } 36 | 37 | @Route('GET', '/test') 38 | @Before(middlewareFixture('three')) 39 | @After(middlewareFixture('four')) 40 | public testMethod(request: Request, response: Response): Response { 41 | return response; 42 | } 43 | 44 | } 45 | 46 | const providers = [ 47 | MiddlewareController, 48 | {provide: Server, useClass: ServerMock}, 49 | {provide: Logger, useClass: LoggerMock}, 50 | {provide: RemoteCli, useClass: RemoteCliMock}, 51 | {provide: AuthService, useClass: AuthServiceMock}, 52 | ReflectiveInjector 53 | ]; 54 | 55 | describe('Middleware Decorators', () => { 56 | 57 | let controller: MiddlewareController; 58 | 59 | beforeEach(() => { 60 | TestBed.configureTestingModule({ providers }); 61 | }); 62 | 63 | it('defines registeredMiddleware on the controller', 64 | inject([MiddlewareController, ReflectiveInjector, Server], 65 | (c: MiddlewareController, i: ReflectiveInjector, s: Server) => { 66 | 67 | controller = c.registerRoutes(s) 68 | .registerInjector(i); 69 | 70 | expect(controller.getMetadata().middleware) 71 | .not 72 | .toBeNull(); 73 | expect(controller.getMetadata().middleware.all.before.length) 74 | .toEqual(2); 75 | expect(controller.getMetadata().middleware.all.after.length) 76 | .toEqual(1); 77 | })); 78 | 79 | it('adds middleware to the call stack', 80 | inject([MiddlewareController, ReflectiveInjector, Server], 81 | (c: MiddlewareController, i: ReflectiveInjector, s: Server) => { 82 | 83 | controller = c.registerRoutes(s) 84 | .registerInjector(i); 85 | 86 | const callStack: any = s.getRoutes() 87 | .reduce((middlewareStackMap: Object, route: RouteConfig) => { 88 | middlewareStackMap[route.methodName] = route.callStack.map((handler: PromiseFactory) => handler.name); 89 | return middlewareStackMap; 90 | }, {}); 91 | 92 | expect(callStack.testMethod) 93 | .toEqual([ 94 | 'mockMiddleware', 95 | 'mockMiddleware', 96 | 'mockMiddleware', 97 | 'testMethod', 98 | 'mockMiddleware', 99 | 'mockMiddleware' 100 | ]); 101 | 102 | })); 103 | 104 | it('calls the stack in the correct order defined by middleware', 105 | async(inject([MiddlewareController, ReflectiveInjector, Server], 106 | (c: MiddlewareController, i: ReflectiveInjector, s: Server) => { 107 | 108 | controller = c.registerRoutes(s) 109 | .registerInjector(i); 110 | 111 | const callStackHandler: any = s.getRoutes() 112 | .find((route: RouteConfig) => route.methodName == 'testMethod').callStackHandler; 113 | 114 | let request = new Request(); 115 | let response = new Response(); 116 | 117 | return callStackHandler(request, response) 118 | .then(() => { 119 | 120 | expect(middlewareCalls) 121 | .toEqual([ 122 | 'one', 123 | 'two', 124 | 'three', 125 | 'four', 126 | 'five', 127 | ]); 128 | 129 | }); 130 | 131 | }))); 132 | 133 | }); 134 | -------------------------------------------------------------------------------- /src/server/migrations/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module server 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { Database } from '../services/database.service'; 6 | import { Logger } from '../../common/services/logger.service'; 7 | /** 8 | * Root class that all implementations of seeders *must* extend. Provides common interface for 9 | * bootstrapper to handle seeding 10 | */ 11 | export abstract class AbstractMigration { 12 | 13 | protected logger:Logger; 14 | constructor(loggerBase:Logger, protected database:Database){ 15 | this.logger = loggerBase.source(this.constructor.name); 16 | } 17 | 18 | /** 19 | * Starts the migration. Returns promise so bootstrapper can wait until finished before starting 20 | * the next migration 21 | */ 22 | public abstract migrate():Promise; 23 | 24 | /** 25 | * Reverts the migration. Returns promise so bootstrapper can wait until finished before rolling 26 | * back the next migration 27 | */ 28 | public abstract rollback():Promise; 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/server/registry/decorators.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module server 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { entityRegistryFunction } from '../../common/registry/decorators'; 6 | import { InjectableMiddlewareFactory } from '../middleware/index'; 7 | 8 | export interface MiddlewareRegistry { 9 | before: InjectableMiddlewareFactory[]; 10 | after: InjectableMiddlewareFactory[]; 11 | } 12 | 13 | export interface ControllerMetadata { 14 | routeBase?:string; 15 | middleware?: { 16 | methods: Map 17 | all: MiddlewareRegistry 18 | } 19 | } 20 | 21 | /** 22 | * @Controller class decorator for registering class with the [[EntityRegistry]] 23 | * 24 | * Example: 25 | * ```typescript 26 | * import { Controller } from '@zerothstack/core/common'; 27 | * import { AbstractController } from '@zerothstack/core/server'; 28 | * 29 | * @Controller() 30 | * export class ExampleController extends AbstractController {} 31 | * ``` 32 | * @param metadata 33 | * @returns {ClassDecorator} 34 | * @constructor 35 | */ 36 | export function Controller(metadata?: ControllerMetadata): ClassDecorator { 37 | return entityRegistryFunction('controller', metadata); 38 | } 39 | /** 40 | * @module common 41 | */ 42 | /** End Typedoc Module Declaration */ 43 | /** 44 | * @Seeder class decorator for registering class with the [[EntityRegistry]] 45 | * 46 | * Example: 47 | * ```typescript 48 | * import { Seeder } from '@zerothstack/core/common'; 49 | * import { AbstractSeeder } from '@zerothstack/core/server'; 50 | * 51 | * @Seeder() 52 | * export class ExampleSeeder extends AbstractSeeder {} 53 | * ``` 54 | * @returns {ClassDecorator} 55 | * @constructor 56 | */ 57 | export function Seeder(): ClassDecorator { 58 | return entityRegistryFunction('seeder'); 59 | } 60 | 61 | /** 62 | * @Migration class decorator for registering class with the [[EntityRegistry]] 63 | * 64 | * Example: 65 | * ```typescript 66 | * import { Migration } from '@zerothstack/core/common'; 67 | * import { AbstractMigration } from '@zerothstack/core/server'; 68 | * 69 | * @Migration() 70 | * export class ExampleMigration extends AbstractMigration {} 71 | * ``` 72 | * @returns {ClassDecorator} 73 | * @constructor 74 | */ 75 | export function Migration(): ClassDecorator { 76 | return entityRegistryFunction('migration'); 77 | } 78 | -------------------------------------------------------------------------------- /src/server/registry/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module server 3 | */ 4 | /** 5 | * Barrel module only for exporting middleware implementations and interfaces 6 | */ 7 | export * from './decorators'; 8 | -------------------------------------------------------------------------------- /src/server/seeders/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module server 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { Logger } from '../../common/services/logger.service'; 6 | /** 7 | * Root class that all implementations of seeders *must* extend. Provides common interface for 8 | * bootstrapper to handle seeding 9 | */ 10 | export abstract class AbstractSeeder { 11 | 12 | protected logger:Logger; 13 | constructor(loggerBase:Logger){ 14 | this.logger = loggerBase.source(this.constructor.name); 15 | } 16 | 17 | /** 18 | * Start the seeding. Returns promise so bootstrapper can wait until finished before starting 19 | * the next seeder 20 | */ 21 | public abstract seed():Promise; 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/server/servers/abstract.server.mock.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module server 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { Server, RouteConfig } from './abstract.server'; 6 | import { RemoteCli } from '../services/remoteCli.service'; 7 | import { Logger } from '../../common/services/logger.service'; 8 | import { Injectable } from '@angular/core'; 9 | import Spy = jasmine.Spy; 10 | 11 | @Injectable() 12 | export class ServerMock extends Server { 13 | 14 | public getEngine(): any { 15 | return undefined; 16 | } 17 | 18 | constructor(logger: Logger, remoteCli: RemoteCli) { 19 | super(logger, remoteCli); 20 | } 21 | 22 | protected registerRouteWithEngine(config: RouteConfig): this { 23 | return this; 24 | } 25 | 26 | protected initialize(): this { 27 | return this; 28 | } 29 | 30 | public startEngine(): Promise { 31 | return Promise.resolve(this); 32 | } 33 | 34 | public registerStaticLoader(webroot?: string): this { 35 | return this; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/server/servers/abstract.server.spec.ts: -------------------------------------------------------------------------------- 1 | import { Server, RouteConfig } from './abstract.server'; 2 | import { RemoteCli } from '../services/remoteCli.service'; 3 | import { Logger } from '../../common/services/logger.service'; 4 | import { inject, TestBed, async } from '@angular/core/testing'; 5 | import { LoggerMock } from '../../common/services/logger.service.mock'; 6 | import { RemoteCliMock } from '../services/remoteCli.service.mock'; 7 | import { Response } from '../controllers/response'; 8 | import Spy = jasmine.Spy; 9 | import { AuthService } from '../services/auth.service'; 10 | import { AuthServiceMock } from '../services/auth.service.mock'; 11 | import { ServerMock } from './abstract.server.mock'; 12 | 13 | describe('Server', () => { 14 | 15 | const providers = [ 16 | {provide: Server, useClass: ServerMock}, 17 | {provide: Logger, useClass: LoggerMock}, 18 | {provide: RemoteCli, useClass: RemoteCliMock}, 19 | {provide: AuthService, useClass: AuthServiceMock}, 20 | ]; 21 | 22 | let cliSpy: Spy; 23 | 24 | beforeEach(() => { 25 | TestBed.configureTestingModule({ providers }); 26 | spyOn(ServerMock.prototype, 'initialize') 27 | .and 28 | .callThrough(); 29 | cliSpy = spyOn(RemoteCliMock.prototype, 'start'); 30 | }); 31 | 32 | it('initializes the server with port and host', inject([Server], (server: Server) => { 33 | 34 | expect((server).initialize) 35 | .toHaveBeenCalled(); 36 | 37 | expect(cliSpy) 38 | .toHaveBeenCalledWith(3001); 39 | 40 | expect(server.getHost()) 41 | .toEqual('http://(localhost):3000'); 42 | 43 | })); 44 | 45 | it('returns the engine', inject([Server], (server: Server) => { 46 | 47 | expect(server.getEngine()) 48 | .toBe(undefined); 49 | 50 | })); 51 | 52 | it('returns the inner http server instance', inject([Server], (server: Server) => { 53 | 54 | expect(server.getHttpServer()) 55 | .toBe(undefined); 56 | 57 | })); 58 | 59 | it('registers routes', inject([Server], (server: Server) => { 60 | 61 | const routeConfig: RouteConfig = { 62 | path: '/test', 63 | methodName: 'test', 64 | method: 'GET', 65 | callStack: [], 66 | callStackHandler: null 67 | }; 68 | 69 | let spy = spyOn(server, 'registerRouteWithEngine'); 70 | 71 | server.register(routeConfig); 72 | 73 | expect(spy) 74 | .toHaveBeenCalledWith(routeConfig); 75 | expect(server.getRoutes()) 76 | .toEqual([routeConfig]); 77 | })); 78 | 79 | it('starts the server running and returns promise', async(inject([Server], (server: Server) => { 80 | 81 | let spy = spyOn(server, 'start') 82 | .and 83 | .callThrough(); 84 | 85 | let response = server.start(); 86 | 87 | expect(spy) 88 | .toHaveBeenCalled(); 89 | 90 | return response.then((onStart: Server) => { 91 | expect(onStart) 92 | .toEqual(server); 93 | }); 94 | 95 | }))); 96 | 97 | it('gets default response object from server', inject([Server], (server: Server) => { 98 | const response: Response = (server).getDefaultResponse(); 99 | 100 | expect(response instanceof Response) 101 | .toBe(true); 102 | 103 | })); 104 | 105 | }); 106 | 107 | 108 | -------------------------------------------------------------------------------- /src/server/servers/abstract.server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module server 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { Injectable } from '@angular/core'; 6 | import { RemoteCli } from '../services/remoteCli.service'; 7 | import { Logger } from '../../common/services/logger.service'; 8 | import { Server as Hapi } from 'hapi'; 9 | import { Response } from '../controllers/response'; 10 | import { Request } from '../controllers/request'; 11 | import { PromiseFactory } from '../../common/util/serialPromise'; 12 | import { Application as Express } from 'express'; 13 | import { Server as HttpServer } from 'http'; 14 | 15 | export type HttpMethod = 'GET' | 'PUT' | 'PATCH' | 'POST' | 'DELETE'; 16 | 17 | export interface RouteConfig { 18 | path: string; 19 | methodName: string; 20 | method: HttpMethod; 21 | callStack: PromiseFactory[]; 22 | callStackHandler: (request: Request, response: Response) => Promise; 23 | } 24 | 25 | /** 26 | * Root class that all implementations of server *must* extends. Provides common interface for 27 | * bootstrapper to handle server startup without caring about underlying implementation 28 | */ 29 | @Injectable() 30 | export abstract class Server { 31 | 32 | /** Hostname eg `localhost`, `example.com` */ 33 | protected host: string; 34 | /** Port number server is running on */ 35 | protected port: number; 36 | 37 | /** `require('http').Server` object from the base class */ 38 | protected httpServer: HttpServer; 39 | 40 | /** All Configured routes */ 41 | public configuredRoutes: RouteConfig[] = []; 42 | 43 | /** Logger instance for the class, initialized with `server` source */ 44 | protected logger: Logger; 45 | 46 | constructor(loggerBase: Logger, remoteCli: RemoteCli) { 47 | 48 | this.logger = loggerBase.source('server'); 49 | 50 | this.host = process.env.APP_HOST || null; //usually should be null to default binding to localhost 51 | this.port = process.env.APP_PORT || 3000; 52 | 53 | this.initialize(); 54 | 55 | remoteCli.start(3001); 56 | } 57 | 58 | /** 59 | * Registration function for routes 60 | * @param config 61 | */ 62 | public register(config: RouteConfig): this { 63 | 64 | this.configuredRoutes.push(config); 65 | return this.registerRouteWithEngine(config); 66 | }; 67 | 68 | /** 69 | * Register the defined route with the engine 70 | * @param config 71 | */ 72 | protected abstract registerRouteWithEngine(config: RouteConfig): this; 73 | 74 | /** 75 | * Initialization function, called before start is called 76 | */ 77 | protected abstract initialize(): this; 78 | 79 | /** 80 | * Kicks off the server using the specific underlying engine 81 | */ 82 | public abstract startEngine(): Promise; 83 | 84 | /** 85 | * Register loader with engine to handle static loading of frontend assets 86 | * @param webroot 87 | */ 88 | public abstract registerStaticLoader(webroot:string):this; 89 | 90 | /** 91 | * Kicks off the server 92 | */ 93 | public start(): Promise { 94 | 95 | this.registerStaticLoader(process.env.WEB_ROOT); 96 | 97 | return this.startEngine(); 98 | }; 99 | 100 | /** 101 | * Retrieves the underlying engine for custom calls 102 | * @returns {Hapi|any} 103 | */ 104 | public abstract getEngine(): Hapi|Express|any; 105 | 106 | /** 107 | * Retrieve the base instance of require('http').Server 108 | * @returns {HttpServer} 109 | */ 110 | public getHttpServer() { 111 | return this.httpServer; 112 | } 113 | 114 | /** 115 | * Get the host name (for logging) 116 | * @returns {string} 117 | */ 118 | public getHost(): string { 119 | return `http://${this.host || '(localhost)'}:${this.port}`; 120 | } 121 | 122 | /** 123 | * Retrieve all configured routes 124 | * @returns {RouteConfig[]} 125 | */ 126 | public getRoutes(): RouteConfig[] { 127 | return this.configuredRoutes; 128 | } 129 | 130 | /** 131 | * Get the default response object 132 | * @returns {Response} 133 | */ 134 | protected getDefaultResponse(): Response { 135 | 136 | return new Response() 137 | // Outputs eg `X-Powered-By: Zeroth` 138 | .header('X-Powered-By', `Zeroth`); 139 | 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /src/server/servers/hapi.server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module server 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { Injectable } from '@angular/core'; 6 | import { Server as Hapi, Request as HapiRequest, IReply, Response as HapiResponse } from 'hapi'; 7 | import { Server, RouteConfig } from './abstract.server'; 8 | import { RemoteCli } from '../services/remoteCli.service'; 9 | import { Logger } from '../../common/services/logger.service'; 10 | import { Response } from '../controllers/response'; 11 | import { Request } from '../controllers/request'; 12 | import { NotImplementedException } from '../../common/exceptions/exceptions'; 13 | 14 | @Injectable() 15 | export class HapiServer extends Server { 16 | 17 | private engine: Hapi; 18 | 19 | constructor(logger: Logger, remoteCli: RemoteCli) { 20 | super(logger, remoteCli); 21 | } 22 | 23 | /** 24 | * @inheritdoc 25 | * @returns {Hapi} 26 | */ 27 | public getEngine(): Hapi { 28 | return this.engine; 29 | } 30 | 31 | /** 32 | * @inheritdoc 33 | * @returns {HapiServer} 34 | */ 35 | protected initialize() { 36 | this.engine = new Hapi(); 37 | 38 | this.engine.connection({ 39 | host: this.host, 40 | port: this.port 41 | }); 42 | 43 | this.httpServer = this.engine.listener; 44 | 45 | return this; 46 | } 47 | 48 | /** 49 | * @inheritdoc 50 | * @returns {any} 51 | * @param routeConfig 52 | */ 53 | protected registerRouteWithEngine(routeConfig: RouteConfig): this { 54 | 55 | if (/[\*\?]/.test(routeConfig.path)) { 56 | throw new Error('Hapi syntax for optional or multi-segment parameters is not supported'); 57 | } 58 | 59 | const config = { 60 | //re-map /path/{param} to /path/{param} (the inverse if needed later is 61 | // .replace(/{([-_a-zA-Z0-9]+).*?}/g, ':$1') 62 | path: routeConfig.path.replace(/:(.+?)(\/|$)/g, "{$1}$2"), 63 | method: routeConfig.method, 64 | handler: (req: HapiRequest, reply: IReply): Promise => { 65 | 66 | let request = new Request((req.raw.req as any), //typings are incorrect, type should be IncomingMessage 67 | Request.extractMapFromDictionary(req.params), 68 | Request.extractMapFromDictionary(req.headers)); 69 | let response = this.getDefaultResponse(); 70 | 71 | return routeConfig.callStackHandler(request, response) 72 | .then((response: Response) => this.send(response, reply)) 73 | .catch((err:Error) => this.sendErr(err, reply)); 74 | } 75 | }; 76 | 77 | this.engine.route(config); 78 | return this; 79 | } 80 | 81 | /** 82 | * Send the response 83 | * @param response 84 | * @param reply 85 | * @return {HapiResponse} 86 | */ 87 | protected send(response: Response, reply: IReply): HapiResponse { 88 | const res = reply(response.body); 89 | 90 | res.code(response.statusCode); 91 | for (var [key, value] of response.headers.entries()) { 92 | res.header(key, value); 93 | } 94 | 95 | return res; 96 | } 97 | 98 | /** 99 | * Send the error response 100 | * @param err 101 | * @param reply 102 | * @return {HapiResponse} 103 | */ 104 | protected sendErr(err: Error, reply: IReply): HapiResponse { 105 | const errorResponse = new Response().data(err); 106 | const res = this.send(errorResponse, reply); 107 | //make sure the status is of error type 108 | if (res.statusCode < 400) { 109 | res.code(500); 110 | } 111 | return res; 112 | } 113 | 114 | /** 115 | * @inheritdoc 116 | */ 117 | public registerStaticLoader(webroot?: string): this { 118 | if (webroot) { 119 | throw new NotImplementedException('Static file listing is not implemented for hapi'); 120 | } 121 | return this; 122 | } 123 | 124 | /** 125 | * @inheritdoc 126 | * @returns {Promise} 127 | */ 128 | public startEngine(): Promise { 129 | 130 | return new Promise((resolve, reject) => { 131 | this.engine.start((err) => { 132 | if (err) { 133 | return reject(err); 134 | } 135 | return resolve(); 136 | }); 137 | }) 138 | .then(() => this); 139 | 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /src/server/servers/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module server 3 | */ 4 | /** 5 | * Barrel module only for exporting server implementations and the abstract `Server` token 6 | */ 7 | export * from './abstract.server'; 8 | export * from './hapi.server'; 9 | 10 | -------------------------------------------------------------------------------- /src/server/services/auth.service.mock.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module server 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { Injectable } from '@angular/core'; 6 | import { Logger } from '../../common/services/logger.service'; 7 | import { Service } from '../../common/registry/decorators'; 8 | import * as jwt from 'jsonwebtoken'; 9 | import { AuthService } from './auth.service'; 10 | 11 | /** 12 | * Class allows developers to register custom commands that can be remote executed in a 13 | * shell environment. Useful for things like migrations and debugging. 14 | */ 15 | @Injectable() 16 | @Service() 17 | export class AuthServiceMock extends AuthService { 18 | 19 | constructor(loggerBase: Logger) { 20 | super(loggerBase); 21 | } 22 | 23 | public verify(jwtToken: string, publicKeyPath: string = '', params: Object = {}): Promise { 24 | 25 | return Promise.resolve(jwt.decode(jwtToken)); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/server/services/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import * as proxyquire from 'proxyquire'; 2 | import { Logger } from '../../common/services/logger.service'; 3 | import { TestBed, async, inject } from '@angular/core/testing'; 4 | import { LoggerMock } from '../../common/services/logger.service.mock'; 5 | import { AuthServiceMock } from './auth.service.mock'; 6 | import { AuthService } from './auth.service'; 7 | 8 | const jwtSpy = jasmine.createSpyObj('jwt', ['verify', 'decode']); 9 | const payload = { 10 | username: 'bob', 11 | }; 12 | jwtSpy.verify.and.returnValue(Promise.resolve(payload)); 13 | jwtSpy.decode.and.returnValue(Promise.resolve(payload)); 14 | 15 | describe('Auth Service (JWT)', () => { 16 | 17 | const fileReadSpy = jasmine.createSpy('readFileSync'); 18 | 19 | const pem = ` 20 | --- BEGIN FAKE KEY --- 21 | --- END FAKE KEY --- 22 | `; 23 | 24 | fileReadSpy.and.returnValue(pem); 25 | 26 | const mockedModule = proxyquire('./auth.service', { 27 | jsonwebtoken: jwtSpy, 28 | fs: { 29 | readFileSync: fileReadSpy 30 | } 31 | }); 32 | 33 | const providers = [ 34 | { 35 | provide: AuthService, 36 | deps: [Logger], 37 | useFactory: (logger: Logger) => { 38 | return new mockedModule.AuthService(logger); 39 | } 40 | }, 41 | {provide: Logger, useClass: LoggerMock}, 42 | ]; 43 | 44 | beforeEach(() => { 45 | TestBed.configureTestingModule({ providers }); 46 | }); 47 | 48 | afterEach(() => { 49 | jwtSpy.verify.calls.reset(); 50 | }); 51 | 52 | it('verifies credentials with jwt and public key', async(inject([AuthService], (s: AuthService) => { 53 | 54 | const jwt = 'pretend.this.is.a.jwt'; 55 | const publicKeyPath = './path/to/key'; 56 | const params = { 57 | iat: 12345679 58 | }; 59 | 60 | const resPromise = s.verify(jwt, publicKeyPath, params); 61 | 62 | expect(fileReadSpy) 63 | .toHaveBeenCalledWith(publicKeyPath); 64 | 65 | expect(jwtSpy.verify) 66 | .toHaveBeenCalledWith(jwt, pem, params, jasmine.any(Function)); 67 | 68 | const callArgCb = jwtSpy.verify.calls.mostRecent().args[3]; 69 | 70 | callArgCb(null, payload); 71 | 72 | return resPromise.then((res) => { 73 | expect(res) 74 | .toEqual(payload); 75 | }); 76 | 77 | }))); 78 | 79 | it('rejects verification of credentials when jwt.verify fails', async(inject([AuthService], (s: AuthService) => { 80 | 81 | const jwt = 'pretend.this.is.an.invalid.jwt'; 82 | const publicKeyPath = './path/to/key'; 83 | 84 | const resPromise = s.verify(jwt, publicKeyPath); 85 | 86 | expect(fileReadSpy) 87 | .toHaveBeenCalledWith(publicKeyPath); 88 | 89 | expect(jwtSpy.verify) 90 | .toHaveBeenCalledWith(jwt, pem, {}, jasmine.any(Function)); 91 | 92 | const callArgCb = jwtSpy.verify.calls.mostRecent().args[3]; 93 | 94 | callArgCb(new Error('jwt error'), payload); 95 | 96 | return resPromise.catch((e) => { 97 | expect(e.message) 98 | .toEqual('jwt error'); 99 | }); 100 | 101 | }))); 102 | 103 | }); 104 | 105 | describe('Auth Service (JWT) Mock', () => { 106 | 107 | const mockedModule = proxyquire('./auth.service.mock', { 108 | jsonwebtoken: jwtSpy, 109 | }); 110 | 111 | const providers = [ 112 | { 113 | provide: AuthServiceMock, 114 | deps: [Logger], 115 | useFactory: (logger: Logger) => { 116 | return new mockedModule.AuthServiceMock(logger); 117 | } 118 | }, 119 | {provide: Logger, useClass: LoggerMock}, 120 | ]; 121 | 122 | beforeEach(() => { 123 | TestBed.configureTestingModule({ providers }); 124 | }); 125 | 126 | it('returns decoded jwt directly without checking', async(inject([AuthServiceMock], (s: AuthServiceMock) => { 127 | 128 | const jwt = 'pretend.this.is.a.valid.jwt'; 129 | 130 | const resPromise = s.verify(jwt); 131 | 132 | expect(jwtSpy.decode) 133 | .toHaveBeenCalledWith(jwt); 134 | 135 | return resPromise.then((res) => { 136 | expect(res) 137 | .toEqual(payload); 138 | }); 139 | 140 | }))); 141 | 142 | }); 143 | -------------------------------------------------------------------------------- /src/server/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module server 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { Injectable } from '@angular/core'; 6 | import { Logger } from '../../common/services/logger.service'; 7 | import { Service } from '../../common/registry/decorators'; 8 | import { AbstractService } from '../../common/services/service'; 9 | import { readFileSync } from 'fs'; 10 | import * as jwt from 'jsonwebtoken'; 11 | 12 | 13 | /** 14 | * Class allows developers to register custom commands that can be remote executed in a 15 | * shell environment. Useful for things like migrations and debugging. 16 | */ 17 | @Injectable() 18 | @Service() 19 | export class AuthService extends AbstractService { 20 | 21 | /** 22 | * Logger instance for the class, initialized with `remote-cli` source 23 | */ 24 | private logger: Logger; 25 | 26 | constructor(loggerBase: Logger) { 27 | super(); 28 | this.logger = loggerBase.source('authentication'); 29 | } 30 | 31 | public verify(jwtToken: string, publicKeyPath: string, params: Object = {}): Promise { 32 | 33 | const pem = readFileSync(publicKeyPath); 34 | 35 | return new Promise((resolve, reject) => { 36 | 37 | jwt.verify(jwtToken, pem, params, (err:Error, decoded:any) => { 38 | if (err){ 39 | return reject(err); 40 | } 41 | 42 | return resolve(decoded); 43 | }); 44 | 45 | }); 46 | 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/server/services/database.service.mock.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module server 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { Injectable } from '@angular/core'; 6 | import { Logger } from '../../common/services/logger.service'; 7 | import { Database } from './database.service'; 8 | import { Service } from '../../common/registry/decorators'; 9 | 10 | /** 11 | * Provides no-side effect mock for Database for use in testing fixtures 12 | */ 13 | @Injectable() 14 | @Service() 15 | export class DatabaseMock extends Database { 16 | 17 | constructor(loggerBase: Logger) { 18 | super(loggerBase); 19 | } 20 | 21 | /** 22 | * Mock initialization, doesn't connect to the database. This means calls to 23 | * `(new DatabaseMock).getConnection()` will return `Promise` 24 | * @returns {Promise} 25 | */ 26 | public initialize(): Promise { 27 | return Promise.resolve(this); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/server/services/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module server 3 | */ 4 | /** 5 | * Barrel module only for exporting core services 6 | */ 7 | export * from './database.service'; 8 | export * from './database.service.mock'; 9 | export * from './remoteCli.service'; 10 | -------------------------------------------------------------------------------- /src/server/services/jwtAuthStrategy.spec.ts: -------------------------------------------------------------------------------- 1 | import { AuthenticationStrategy, RemoteCliContext } from './remoteCli.service'; 2 | import { LoggerMock } from '../../common/services/logger.service.mock'; 3 | import { jwtAuthStrategyFactory } from './jwtAuthStrategy'; 4 | import * as chalk from 'chalk'; 5 | 6 | import Spy = jasmine.Spy; 7 | 8 | describe('JWT Authentication Strategy', () => { 9 | 10 | const loggerMock = new LoggerMock(); 11 | const authService = jasmine.createSpyObj('authService', ['verify']); 12 | 13 | const payload = { 14 | username: 'bob', 15 | }; 16 | authService.verify.and.returnValue(Promise.resolve(payload)); 17 | 18 | process.env.PATH_ROOT = '/tmp'; //tmp for testing 19 | 20 | const context: RemoteCliContext = { 21 | logger: loggerMock, 22 | authService: authService, 23 | }; 24 | 25 | let authStrategy: AuthenticationStrategy; 26 | 27 | beforeEach(() => { 28 | authStrategy = jwtAuthStrategyFactory(context); 29 | }); 30 | 31 | it('rejects callback when jwt is not passeed', () => { 32 | 33 | const callbackSpy = jasmine.createSpy('cb'); 34 | 35 | authStrategy(null, null)({client: {}}, callbackSpy); 36 | 37 | expect(callbackSpy) 38 | .toHaveBeenCalledWith("JWT was not passed in connection request", false); 39 | 40 | }); 41 | 42 | it('verifies with the passed jwt and authentication key path', (cb) => { 43 | 44 | const jwt = 'pretend.this.is.a.jwt'; 45 | const publicKeyPath = './path/to/key'; 46 | 47 | const vantageScope = jasmine.createSpyObj('vantageScope', ['log']); 48 | 49 | const authenticator = authStrategy(null, null) 50 | .bind(vantageScope); 51 | 52 | authenticator({client: {jwt, publicKeyPath, columns: 100}}, (message:string, isSuccess:boolean) => { 53 | 54 | expect(authService.verify).toHaveBeenCalledWith(jwt, '/tmp/path/to/key'); 55 | 56 | expect(vantageScope.log) 57 | .toHaveBeenCalledWith(chalk.grey(`You were authenticated with a JSON Web token verified against the public key at /tmp/path/to/key`)); 58 | 59 | expect(isSuccess).toBe(true); 60 | expect(message).toBe(null); 61 | cb(); 62 | }); 63 | 64 | }); 65 | 66 | 67 | it('rejects the callback if loading the auth service fails', (cb) => { 68 | 69 | const vantageScope = jasmine.createSpyObj('vantageScope', ['log']); 70 | 71 | const authenticator = authStrategy(null, null) 72 | .bind(vantageScope); 73 | 74 | const jwt = 'pretend.this.is.a.jwt'; 75 | const publicKeyPath = './path/to/key'; 76 | 77 | authService.verify.and.throwError('authentication lib error'); 78 | 79 | authenticator({client: {jwt, publicKeyPath}}, (message:string, isSuccess:boolean) => { 80 | 81 | expect(isSuccess).toBe(false); 82 | expect(message).toBe('authentication lib error'); 83 | cb(); 84 | }); 85 | 86 | }); 87 | 88 | it('rejects the callback when the authentication service errors', (cb) => { 89 | 90 | const vantageScope = jasmine.createSpyObj('vantageScope', ['log']); 91 | 92 | const authenticator = authStrategy(null, null) 93 | .bind(vantageScope); 94 | 95 | const jwt = 'pretend.this.is.a.jwt'; 96 | const publicKeyPath = './path/to/key'; 97 | 98 | authService.verify.and.returnValue(Promise.reject(new Error('authentication failed'))); 99 | 100 | authenticator({client: {jwt, publicKeyPath}}, (message:string, isSuccess:boolean) => { 101 | 102 | expect(isSuccess).toBe(false); 103 | expect(message).toBe('authentication failed'); 104 | cb(); 105 | }); 106 | 107 | }); 108 | 109 | }); 110 | -------------------------------------------------------------------------------- /src/server/services/jwtAuthStrategy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module server 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { bannerBg } from '../../common/util/banner'; 6 | import * as chalk from 'chalk'; 7 | import * as path from 'path'; 8 | import { 9 | AuthenticationStrategy, 10 | RemoteCliContext, 11 | AuthenticationStrategyFactory, 12 | AuthenticationCallback 13 | } from './remoteCli.service'; 14 | 15 | export const jwtAuthStrategyFactory:AuthenticationStrategyFactory = (remoteCliContext: RemoteCliContext): AuthenticationStrategy => { 16 | return function (vantage: any, options: any) { 17 | return async function (args: {client: {jwt: string, publicKeyPath: string, columns: number}}, cb: AuthenticationCallback) { 18 | 19 | try { 20 | remoteCliContext.logger.silly.debug('Passed client arguments: ', args); 21 | 22 | const token: string = args.client.jwt; 23 | let keyPath: string = args.client.publicKeyPath; 24 | 25 | if (!token) { 26 | return cb("JWT was not passed in connection request", false); 27 | } 28 | 29 | if (process.env.PATH_ROOT){ 30 | keyPath = path.resolve(process.env.PATH_ROOT, keyPath); 31 | } 32 | 33 | remoteCliContext.logger.info(`Authenticating JSON web token against public key [${keyPath}]`); 34 | 35 | const payload = await remoteCliContext.authService.verify(token, keyPath); 36 | 37 | remoteCliContext.logger.info(`${payload.username} has been authenticated with token`) 38 | .debug('Token:', token); 39 | 40 | let displayBanner = `Hi ${payload.username}, Welcome to Zeroth runtime cli.`; 41 | if (args.client.columns >= 80) { 42 | displayBanner = bannerBg(undefined, token); 43 | } 44 | 45 | this.log(chalk.grey(`You were authenticated with a JSON Web token verified against the public key at ${keyPath}`)); 46 | this.log(displayBanner); 47 | this.log(` Type 'help' for a list of available commands`); 48 | 49 | return cb(null, true); 50 | 51 | } catch (e) { 52 | remoteCliContext.logger.error('Authentication error', e.message).debug(e.stack); 53 | cb(e.message, false); 54 | } 55 | }; 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /src/server/services/remoteCli.service.mock.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module server 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { Injectable, Injector } from '@angular/core'; 6 | import { RemoteCli, ConnectedSocketCallback } from './remoteCli.service'; 7 | import { Logger } from '../../common/services/logger.service'; 8 | import { Service } from '../../common/registry/decorators'; 9 | 10 | import { AuthService } from './auth.service'; 11 | 12 | /** 13 | * Provides no-side effect mock for RemoteCli for use in testing fixtures 14 | */ 15 | @Injectable() 16 | @Service() 17 | export class RemoteCliMock extends RemoteCli { 18 | 19 | constructor(loggerBase: Logger, injector: Injector, authService:AuthService) { 20 | super(loggerBase, injector, authService); 21 | } 22 | 23 | /** 24 | * Override of parent command register method 25 | * @returns {RemoteCliMock} 26 | */ 27 | protected registerCommands(): this { 28 | return this; 29 | } 30 | 31 | /** 32 | * Override of parent start method 33 | * @param port 34 | * @param callback 35 | * @returns {RemoteCliMock} 36 | */ 37 | public start(port: number, callback?: ConnectedSocketCallback): this { 38 | return this; 39 | } 40 | 41 | /** 42 | * This overrides the parent method so that vantage is not initialised in tests 43 | * @returns {RemoteCliMock} 44 | */ 45 | public initialize(): this { 46 | return this.registerCommands(); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/server/services/vantage.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'vantage' 2 | -------------------------------------------------------------------------------- /src/server/stores/db.store.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module server 3 | */ 4 | /** End Typedoc Module Declaration */ 5 | import { Injectable, Injector } from '@angular/core'; 6 | import { AbstractModel, ModelStatic, identifier } from '../../common/models/model'; 7 | import { Database } from '../services/database.service'; 8 | import { Logger } from '../../common/services/logger.service'; 9 | import { AbstractStore, Query } from '../../common/stores/store'; 10 | import { Collection } from '../../common/models/collection'; 11 | import { Repository, Connection } from 'typeorm'; 12 | import { NotFoundException } from '../../common/exceptions/exceptions'; 13 | 14 | /** 15 | * Database store should be extended with a specific implementation for a model. Interacts with 16 | * TypeORM's repository to handle CRUD with the database 17 | */ 18 | @Injectable() 19 | export abstract class DatabaseStore extends AbstractStore { 20 | 21 | /** 22 | * The TypeORM repository instance 23 | */ 24 | protected repositoryPromise: Promise>; 25 | 26 | /** 27 | * Logger for the class, initialized with source 28 | */ 29 | protected logger: Logger; 30 | 31 | constructor(modelStatic: ModelStatic, injector: Injector, protected database: Database, protected loggerBase: Logger) { 32 | super(modelStatic, injector); 33 | this.logger = loggerBase.source('DB Store'); 34 | } 35 | 36 | /** 37 | * Retrieve the TypeORM repository for this store's modelStatic. 38 | * This promise is cached so the same repository instance is always returned 39 | * @returns {Promise>} 40 | */ 41 | public getRepository(): Promise> { 42 | if (!this.repositoryPromise) { 43 | this.repositoryPromise = this.database.getConnection() 44 | .then((connection: Connection) => connection.getRepository(this.modelStatic)) 45 | } 46 | 47 | return this.repositoryPromise; 48 | } 49 | 50 | /** 51 | * @inheritdoc 52 | */ 53 | public async initialized(): Promise { 54 | await this.getRepository(); 55 | return this; 56 | } 57 | 58 | /** 59 | * @inheritdoc 60 | */ 61 | public async findOne(id: identifier): Promise { 62 | 63 | const repo = await this.getRepository(); 64 | const model: T = await repo.findOneById(id); 65 | 66 | if (!model) { 67 | throw new NotFoundException(`${this.modelStatic.name} not found with id [${id}]`); 68 | } 69 | 70 | return model; 71 | } 72 | 73 | /** 74 | * @inheritdoc 75 | */ 76 | public async findMany(query?: Query): Promise> { 77 | 78 | try { 79 | const repo = await this.getRepository(); 80 | 81 | const entityArray: T[] = await repo.find({ 82 | //@todo define query interface and restrict count with pagination 83 | }); 84 | 85 | if (!entityArray.length) { 86 | throw new NotFoundException(`No ${this.modelStatic.name} found with query params [${JSON.stringify(query)}]`); 87 | } 88 | 89 | return new Collection(entityArray); 90 | 91 | } catch (e) { 92 | this.logger.error(e); 93 | throw e; 94 | } 95 | } 96 | 97 | /** 98 | * @inheritdoc 99 | */ 100 | public async saveOne(model: T): Promise { 101 | const repo = await this.getRepository(); 102 | return repo.persist(model); 103 | } 104 | 105 | /** 106 | * @inheritdoc 107 | */ 108 | public async deleteOne(model: T): Promise { 109 | const repo = await this.getRepository(); 110 | return repo.remove(model); 111 | } 112 | 113 | /** 114 | * @inheritdoc 115 | */ 116 | public async hasOne(model: T): Promise { 117 | const repo = await this.getRepository(); 118 | try { 119 | await repo.findOneById(model.getIdentifier()); 120 | return true; 121 | } catch (e) { 122 | return false; 123 | } 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /src/server/stores/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Barrel module only for exporting abstract (server-only) store implementations 3 | * @module server 4 | */ 5 | /** End Typedoc Module Declaration */ 6 | export * from './db.store'; 7 | -------------------------------------------------------------------------------- /tsconfig.browser.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "removeComments": false, 10 | "noImplicitAny": true, 11 | "suppressImplicitAnyIndexErrors": true, 12 | "sourceRoot": "src", 13 | "outDir": "dist/browser", 14 | "skipLibCheck": true, 15 | "types": [ 16 | "jasmine", 17 | "node" 18 | ] 19 | }, 20 | "exclude": [ 21 | "node_modules", 22 | "lib", 23 | "dist", 24 | "src/server" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "removeComments": false, 10 | "noImplicitAny": true, 11 | "suppressImplicitAnyIndexErrors": true, 12 | "declaration": true, 13 | "outDir": "lib", 14 | "sourceRoot": "src", 15 | "skipLibCheck": true, 16 | "types": [ 17 | "jasmine", 18 | "socket.io" 19 | ] 20 | }, 21 | "exclude": [ 22 | "node_modules", 23 | "lib", 24 | "dist" 25 | ], 26 | "include": [ 27 | "src/**/*" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "removeComments": false, 10 | "noImplicitAny": true, 11 | "suppressImplicitAnyIndexErrors": true, 12 | "declaration": false, 13 | "outDir": "dist/server", 14 | "sourceRoot": "src", 15 | "inlineSources": true, 16 | "skipLibCheck": true, 17 | "types": [ 18 | "node", 19 | "jasmine", 20 | "socket.io" 21 | ] 22 | }, 23 | "exclude": [ 24 | "node_modules", 25 | "lib", 26 | "dist", 27 | "src/browser" 28 | ], 29 | "include": [ 30 | "src/**/*" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "max-line-length": [true, 100], 4 | "no-inferrable-types": true, 5 | "class-name": true, 6 | "comment-format": [ 7 | true, 8 | "check-space" 9 | ], 10 | "indent": [ 11 | true, 12 | "spaces" 13 | ], 14 | "eofline": true, 15 | "no-duplicate-variable": true, 16 | "no-eval": true, 17 | "no-arg": true, 18 | "no-internal-module": true, 19 | "no-bitwise": true, 20 | "no-shadowed-variable": true, 21 | "no-unused-expression": true, 22 | "no-unused-variable": true, 23 | "one-line": [ 24 | true, 25 | "check-catch", 26 | "check-else", 27 | "check-open-brace", 28 | "check-whitespace" 29 | ], 30 | "quotemark": [ 31 | true, 32 | "single", 33 | "avoid-escape" 34 | ], 35 | "semicolon": [true, "always"], 36 | "typedef-whitespace": [ 37 | true, 38 | { 39 | "call-signature": "nospace", 40 | "index-signature": "nospace", 41 | "parameter": "nospace", 42 | "property-declaration": "nospace", 43 | "variable-declaration": "nospace" 44 | } 45 | ], 46 | "curly": true, 47 | "variable-name": [ 48 | true, 49 | "ban-keywords", 50 | "check-format", 51 | "allow-trailing-underscore" 52 | ], 53 | "whitespace": [ 54 | true, 55 | "check-branch", 56 | "check-decl", 57 | "check-operator", 58 | "check-separator", 59 | "check-type" 60 | ] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /zeroth.js: -------------------------------------------------------------------------------- 1 | let { ZerothProject } = require('@zerothstack/toolchain'); 2 | 3 | const project = new ZerothProject(__dirname) 4 | .configureDeployment({ 5 | docs: { 6 | branch: 'master', 7 | repo: 'git@github.com:zerothstack/zerothstack.github.io.git' 8 | } 9 | }) 10 | .configureDocs({ 11 | meta: { 12 | gaCode: 'UA-88015201-1' 13 | } 14 | }) 15 | .configureSocial({ 16 | twitter: 'zeroth', 17 | gitter: 'zerothstack/zeroth' 18 | }) 19 | ; 20 | 21 | module.exports = project; 22 | --------------------------------------------------------------------------------