Repository: brainhubeu/hadron Branch: master Commit: 28cfe5143fad Files: 315 Total size: 283.2 KB Directory structure: gitextract_tdgrkyma/ ├── .circleci/ │ └── config.yml ├── .dockerignore ├── .gitignore ├── .gitlab-ci.yml ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .rebuild.ts ├── Dockerfile ├── LICENSE ├── README.md ├── docker-compose.yml ├── entrypoint.sh ├── features/ │ ├── step_definitions/ │ │ ├── steps.ts │ │ └── theBestSteps.ts │ ├── support/ │ │ ├── hooks/ │ │ │ └── mock.ts │ │ ├── scripts/ │ │ │ ├── Client.ts │ │ │ └── Response.ts │ │ └── world.ts │ └── theBest.feature ├── lerna.json ├── ormconfig.json ├── package-test.sh ├── package.json ├── packages/ │ ├── hadron-auth/ │ │ ├── LICENSE │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ ├── src/ │ │ │ ├── HadronAuth.ts │ │ │ ├── IRoute.ts │ │ │ ├── ISecurityOptions.ts │ │ │ ├── __tests__/ │ │ │ │ ├── HadronAuth.ts │ │ │ │ ├── hierarchyProvider.ts │ │ │ │ └── urlGlob.ts │ │ │ ├── constants.ts │ │ │ ├── declarations.d.ts │ │ │ ├── helpers/ │ │ │ │ ├── flattenDeep.ts │ │ │ │ └── urlGlob.ts │ │ │ ├── hierarchyProvider.ts │ │ │ ├── password/ │ │ │ │ ├── IHashMethod.ts │ │ │ │ ├── __tests__/ │ │ │ │ │ └── hashMethodProvider.ts │ │ │ │ ├── bcrypt/ │ │ │ │ │ ├── IBcryptOptions.ts │ │ │ │ │ ├── __tests__/ │ │ │ │ │ │ └── bcrypt.ts │ │ │ │ │ └── bcrypt.ts │ │ │ │ └── hashMethodProvider.ts │ │ │ └── providers/ │ │ │ └── expressMiddlewareAuthorization.ts │ │ └── tsconfig.json │ ├── hadron-core/ │ │ ├── LICENSE │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ ├── src/ │ │ │ ├── __tests__/ │ │ │ │ └── hadronCore.test.ts │ │ │ ├── constants/ │ │ │ │ └── eventNames.ts │ │ │ ├── container/ │ │ │ │ ├── __tests__/ │ │ │ │ │ ├── container.ts │ │ │ │ │ └── containerItem.ts │ │ │ │ ├── container.ts │ │ │ │ ├── containerItem.ts │ │ │ │ ├── lifecycle.ts │ │ │ │ └── types.ts │ │ │ ├── declarations.d.ts │ │ │ ├── errors/ │ │ │ │ ├── IncorrectContainerKeyNameError.ts │ │ │ │ └── LoadingPackageError.ts │ │ │ ├── hadronCore.ts │ │ │ └── helpers/ │ │ │ ├── __tests__/ │ │ │ │ └── isVarName.ts │ │ │ └── isVarName.ts │ │ └── tsconfig.json │ ├── hadron-demo/ │ │ ├── LICENSE │ │ ├── README.md │ │ ├── declarations.d.ts │ │ ├── entity/ │ │ │ ├── Role.ts │ │ │ ├── Team.ts │ │ │ ├── User.ts │ │ │ └── validation/ │ │ │ ├── schemas.ts │ │ │ ├── team/ │ │ │ │ ├── insertTeam.json │ │ │ │ └── updateTeam.json │ │ │ ├── user/ │ │ │ │ ├── insertUser.json │ │ │ │ └── updateUser.json │ │ │ └── validate.ts │ │ ├── event-emitter/ │ │ │ ├── case1/ │ │ │ │ └── index.ts │ │ │ ├── case2/ │ │ │ │ └── index.ts │ │ │ ├── case3/ │ │ │ │ └── index.ts │ │ │ └── config.ts │ │ ├── express-demo/ │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── logger/ │ │ │ ├── adapters/ │ │ │ │ └── winstonAdapter.ts │ │ │ └── index.ts │ │ ├── package.json │ │ ├── performance-tests/ │ │ │ ├── README.md │ │ │ ├── teamService/ │ │ │ │ ├── getTeams.yml │ │ │ │ ├── insertTeam.yml │ │ │ │ ├── team.yml │ │ │ │ └── updateTeam.yml │ │ │ └── userService/ │ │ │ ├── getUsers.yml │ │ │ ├── insertUser.yml │ │ │ ├── updateUser.yml │ │ │ └── user.yml │ │ ├── routing/ │ │ │ ├── home.config.js │ │ │ ├── nested-routes.config.js │ │ │ ├── team.config.js │ │ │ └── user.config.js │ │ ├── security/ │ │ │ ├── loginRoute.ts │ │ │ └── securedRoutesConfig.ts │ │ ├── serialization/ │ │ │ ├── routing/ │ │ │ │ ├── index.ts │ │ │ │ ├── princesses.ts │ │ │ │ └── unicorns.ts │ │ │ ├── schemas/ │ │ │ │ ├── princess.json │ │ │ │ └── unicorn.json │ │ │ ├── serialization-demo.ts │ │ │ └── unicorns-and-princesses.ts │ │ ├── services/ │ │ │ ├── teamService.ts │ │ │ └── userService.ts │ │ ├── tsconfig.json │ │ └── typeorm-demo/ │ │ └── index.ts │ ├── hadron-error-handler/ │ │ ├── LICENSE │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ ├── src/ │ │ │ └── errorHandler.ts │ │ └── tsconfig.json │ ├── hadron-events/ │ │ ├── LICENSE │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ ├── src/ │ │ │ ├── __tests__/ │ │ │ │ └── eventManagerProvider.ts │ │ │ ├── constants.ts │ │ │ ├── eventManagerProvider.ts │ │ │ ├── helpers/ │ │ │ │ └── functionHelper.ts │ │ │ ├── registerProcessEvents.ts │ │ │ └── types.ts │ │ └── tsconfig.json │ ├── hadron-express/ │ │ ├── LICENSE │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ ├── src/ │ │ │ ├── __tests__/ │ │ │ │ ├── __mocks__/ │ │ │ │ │ └── routes/ │ │ │ │ │ └── route.js │ │ │ │ ├── createContainerProxy.ts │ │ │ │ ├── generateMiddlewares.ts │ │ │ │ └── hadronToExpress.ts │ │ │ ├── constants/ │ │ │ │ ├── eventNames.ts │ │ │ │ └── routing.ts │ │ │ ├── createContainerProxy.ts │ │ │ ├── declarations.d.ts │ │ │ ├── errors/ │ │ │ │ ├── CreateRouteError.ts │ │ │ │ ├── GenerateMiddlewareError.ts │ │ │ │ ├── InvalidRouteMethodError.ts │ │ │ │ └── NoRouterMethodSpecifiedError.ts │ │ │ ├── generateMiddlewares.ts │ │ │ ├── hadronToExpress.ts │ │ │ ├── handleResponseSpec.ts │ │ │ ├── prepareRequest.ts │ │ │ └── types.ts │ │ └── tsconfig.json │ ├── hadron-file-locator/ │ │ ├── LICENSE │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ ├── src/ │ │ │ ├── __tests__/ │ │ │ │ ├── file-locator.ts │ │ │ │ └── mock/ │ │ │ │ ├── app/ │ │ │ │ │ ├── config/ │ │ │ │ │ │ ├── config.js │ │ │ │ │ │ ├── config.json │ │ │ │ │ │ ├── config.xml │ │ │ │ │ │ ├── config_development.js │ │ │ │ │ │ ├── config_development.json │ │ │ │ │ │ └── config_test.js │ │ │ │ │ ├── ext/ │ │ │ │ │ │ ├── config.js │ │ │ │ │ │ ├── config.json │ │ │ │ │ │ └── config.xml │ │ │ │ │ └── universal/ │ │ │ │ │ ├── dog.config.js │ │ │ │ │ ├── other.json │ │ │ │ │ ├── team.config.js │ │ │ │ │ ├── test.js │ │ │ │ │ └── user.config.js │ │ │ │ └── plugins/ │ │ │ │ ├── plugin1/ │ │ │ │ │ └── config/ │ │ │ │ │ ├── config.js │ │ │ │ │ ├── config_development.js │ │ │ │ │ ├── config_development.ts │ │ │ │ │ └── config_test.js │ │ │ │ ├── plugin2/ │ │ │ │ │ └── config/ │ │ │ │ │ ├── config.js │ │ │ │ │ ├── config_development.js │ │ │ │ │ └── config_test.js │ │ │ │ └── plugin3/ │ │ │ │ └── config/ │ │ │ │ ├── config.js │ │ │ │ ├── config_development.js │ │ │ │ └── config_test.js │ │ │ ├── declarations.d.ts │ │ │ ├── file-locator.ts │ │ │ └── glob-promise.ts │ │ └── tsconfig.json │ ├── hadron-json-provider/ │ │ ├── LICENSE │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ ├── src/ │ │ │ ├── __tests__/ │ │ │ │ ├── config-json-provider.ts │ │ │ │ ├── js-loader.ts │ │ │ │ ├── json-loader.ts │ │ │ │ ├── json-provider.ts │ │ │ │ ├── mock/ │ │ │ │ │ ├── app/ │ │ │ │ │ │ ├── config/ │ │ │ │ │ │ │ ├── config.js │ │ │ │ │ │ │ ├── config.json │ │ │ │ │ │ │ ├── config.xml │ │ │ │ │ │ │ ├── config_development.js │ │ │ │ │ │ │ ├── config_development.json │ │ │ │ │ │ │ └── config_test.js │ │ │ │ │ │ ├── ext/ │ │ │ │ │ │ │ ├── config.js │ │ │ │ │ │ │ ├── config.json │ │ │ │ │ │ │ └── config.xml │ │ │ │ │ │ └── universal/ │ │ │ │ │ │ ├── dog.config.js │ │ │ │ │ │ ├── exportDefaultConfig.js │ │ │ │ │ │ ├── other.json │ │ │ │ │ │ ├── team.config.js │ │ │ │ │ │ ├── test.js │ │ │ │ │ │ └── user.config.js │ │ │ │ │ └── plugins/ │ │ │ │ │ ├── plugin1/ │ │ │ │ │ │ └── config/ │ │ │ │ │ │ ├── config.js │ │ │ │ │ │ ├── config_development.js │ │ │ │ │ │ ├── config_development.ts │ │ │ │ │ │ └── config_test.js │ │ │ │ │ ├── plugin2/ │ │ │ │ │ │ └── config/ │ │ │ │ │ │ ├── config.js │ │ │ │ │ │ ├── config_development.js │ │ │ │ │ │ └── config_test.js │ │ │ │ │ └── plugin3/ │ │ │ │ │ └── config/ │ │ │ │ │ ├── config.js │ │ │ │ │ ├── config_development.js │ │ │ │ │ └── config_test.js │ │ │ │ └── xml-loader.ts │ │ │ ├── declarations.d.ts │ │ │ └── json-provider.ts │ │ └── tsconfig.json │ ├── hadron-logger/ │ │ ├── LICENSE │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ ├── src/ │ │ │ ├── __tests__/ │ │ │ │ └── logger.ts │ │ │ ├── adapters/ │ │ │ │ ├── __tests__/ │ │ │ │ │ └── bunyan.ts │ │ │ │ ├── bunyan.ts │ │ │ │ └── index.ts │ │ │ ├── errors/ │ │ │ │ ├── ConfigNotDefinedError.ts │ │ │ │ ├── CouldNotRegisterLoggerInContainerError.ts │ │ │ │ ├── LoggerAdapterNotDefinedError.ts │ │ │ │ └── LoggerNameIsRequiredError.ts │ │ │ ├── logger.ts │ │ │ └── types.ts │ │ └── tsconfig.json │ ├── hadron-oauth/ │ │ ├── .eslintrc │ │ ├── .gitignore │ │ ├── LICENSE │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ ├── src/ │ │ │ ├── __tests__/ │ │ │ │ ├── facebook.ts │ │ │ │ ├── formatQueryString.ts │ │ │ │ ├── github.ts │ │ │ │ └── google.ts │ │ │ ├── facebook/ │ │ │ │ ├── redirect.ts │ │ │ │ └── token.ts │ │ │ ├── github/ │ │ │ │ ├── redirect.ts │ │ │ │ └── token.ts │ │ │ ├── google/ │ │ │ │ ├── redirect.ts │ │ │ │ └── token.ts │ │ │ ├── types.ts │ │ │ └── util/ │ │ │ ├── constants.ts │ │ │ └── formatQueryString.ts │ │ └── tsconfig.json │ ├── hadron-serialization/ │ │ ├── LICENSE │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ ├── src/ │ │ │ ├── __tests__/ │ │ │ │ ├── mocks.ts │ │ │ │ ├── schema-provider.ts │ │ │ │ └── serializer.ts │ │ │ ├── constants.ts │ │ │ ├── declarations.d.ts │ │ │ ├── schema-provider.ts │ │ │ ├── serializer.ts │ │ │ └── types.ts │ │ └── tsconfig.json │ ├── hadron-typeorm/ │ │ ├── LICENSE │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ ├── src/ │ │ │ ├── __tests__/ │ │ │ │ ├── connectionHelper.ts │ │ │ │ └── mocks/ │ │ │ │ ├── entity/ │ │ │ │ │ ├── Team.ts │ │ │ │ │ ├── User.ts │ │ │ │ │ └── UserStatus.ts │ │ │ │ └── schema/ │ │ │ │ └── User.ts │ │ │ ├── connectionHelper.ts │ │ │ ├── constants.ts │ │ │ └── types.ts │ │ └── tsconfig.json │ ├── hadron-utils/ │ │ ├── LICENSE │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ ├── src/ │ │ │ ├── __tests__/ │ │ │ │ └── getArgs.ts │ │ │ └── getArgs.ts │ │ └── tsconfig.json │ └── hadron-validation/ │ ├── LICENSE │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── src/ │ │ ├── __tests__/ │ │ │ ├── __mocks__/ │ │ │ │ ├── Team.ts │ │ │ │ ├── User.ts │ │ │ │ ├── declarations.d.ts │ │ │ │ ├── test-schemas/ │ │ │ │ │ ├── email.json │ │ │ │ │ ├── person.json │ │ │ │ │ ├── team.json │ │ │ │ │ ├── user.json │ │ │ │ │ └── users.json │ │ │ │ ├── test-schemas.ts │ │ │ │ └── test-validate.ts │ │ │ └── validate.ts │ │ └── validator-factory.ts │ └── tsconfig.json ├── pm2.config.js ├── scripts/ │ ├── clean │ └── copy-tsconfig ├── test.sh ├── tools/ │ └── testSetup.ts ├── tsconfig.json └── tslint.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .circleci/config.yml ================================================ # Javascript Node CircleCI 2.0 configuration file # # Check https://circleci.com/docs/2.0/language-javascript/ for more details # version: 2 jobs: build_test: docker: - image: circleci/node:9.11.1 working_directory: ~/repo steps: - checkout - run: npm install - run: npm run test workflows: version: 2 test: jobs: - build_test ================================================ FILE: .dockerignore ================================================ node_modules docs features .npmrc .nvmrc ================================================ FILE: .gitignore ================================================ /dist /logs /npm-debug.log /node_modules .DS_Store .idea/ .env .vscode node_modules dist package-lock.json .nyc_output /**/package-lock.json /**/node_modules /**/dist package-lock.json lerna-debug.log *.swp ================================================ FILE: .gitlab-ci.yml ================================================ image: node:8.9.0 stages: - build - lint - test before_script: - npm install - apt-get update - apt-get install -y netcat # This folder is cached between builds # http://docs.gitlab.com/ce/ci/yaml/README.html#cache cache: paths: - node_modules/ test: script: - npm run build - npm run test lint: script: - npm run lint build: script: - npm run build ================================================ FILE: .npmrc ================================================ save-exact=true package-lock=false ================================================ FILE: .nvmrc ================================================ 9 ================================================ FILE: .prettierignore ================================================ /.history /node_modules /.vscode /.idea /dist */**/*.temp */**/*.temp.* */**/package.json */**/package-lock.json package.json package-lock.json ================================================ FILE: .prettierrc ================================================ { "trailingComma": "all", "singleQuote": true, "arrowParens" :"always" } ================================================ FILE: .rebuild.ts ================================================ ================================================ FILE: Dockerfile ================================================ FROM keymetrics/pm2:8-alpine COPY .env /.env COPY entrypoint.sh / COPY package.json /usr/src/app/package.json WORKDIR /tmp/app RUN apk --no-cache add git openssh-client curl COPY . /tmp/app RUN export $(cat /.env | xargs) && NODE_ENV=development npm install --progress=false RUN export $(cat /.env | xargs) && /tmp/app/node_modules/.bin/tsc --project /tmp/app --outDir /usr/src/app/ RUN export $(cat /.env | xargs) && npm install --progress=false --prefix /usr/src/app/ WORKDIR /usr/src/app CMD ["npm", "run", "start:production"] ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2017 Brainhub Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

Hadron Logo

[![CircleCI](https://circleci.com/gh/brainhubeu/hadron.svg?style=svg)](https://circleci.com/gh/brainhubeu/hadron) ## Why? **Hadron's purpose is to facilitate the building of Node.js applications:** ### Low-level framework-agnostic Your application is built independently from other frameworks (Express, Koa). Hadron creates a layer between HTTP requests and your app written in plain Javascript. Hadron abstracts away underlying request and response objects, providing simple data structures as input/output of your routes' handlers, making them simple to test and easy to deal with. ### Dependency injection The dependency injection pattern enables you to easily change interface implementation. Hadron gives us the power to create SOLID applications. Containers as a dependency management solution provides a convenient way to access all dependencies in functions. ### Modular structure The modular structure enables you to add/remove packages or create your own extensions. Hadron provides a complete solution for request processing using separate packages. Current packages: * security management * input validation * database integration (through TypeORM) * data serialization * logging * events handling * CLI tool Built with TypeScript, but it's primary target is JavaScript apps. Hadron’s API embraces current ECMAScript standards, with the cherry of good IDE support via codebase types declarations on top. > To read more about hadron check out our article: [How to use Hadron?](https://brainhub.eu/blog/building-api-expressjs-and-hadron/) ## Installation * Install Node.js. We recommend using the latest version, installation details on [nodejs.org](https://nodejs.org) * Install following modules from npm: ```bash npm install @brainhubeu/hadron-core @brainhubeu/hadron-express express --save ``` ## Hello World app Let's start with a simple Hello World app. It will give you a quick grasp of the framework. ```javascript const hadron = require('@brainhubeu/hadron-core').default; const express = require('express'); const port = 8080; const expressApp = express(); const config = { routes: { helloWorldRoute: { path: '/', callback: () => 'Hello world!', methods: ['get'], }, }, }; hadron(expressApp, [require('@brainhubeu/hadron-express')], config).then(() => { expressApp.listen(port, () => console.log(`Listening on http://localhost:${port}`), ); }); ``` ## Documentation Hadron documentation can be found at [http://hadron.pro](http://hadron.pro) ## Getting Started #### Requirements * Installed GIT * Installed node.js (we recommend using [nvm](https://github.com/creationix/nvm) to run multiple versions of node). We recommend using latest version of node. If you want to use older versions you may need to add [babel-polyfill](https://babeljs.io/docs/usage/polyfill/) to use [some features](http://node.green/). #### Clone it ```sh git clone git@github.com:brainhubeu/hadron.git cd brainhub-framework-app ``` #### Install dependencies ```sh npm install ``` #### Run development server ```sh npm run dev ``` #### Run production server ```sh npm start ``` ## Running tests #### All tests ```sh npm run test # or PORT=8181 npm run test ``` #### Unit tests Run unit tests for each package: ```sh npm run test:unit ``` Run unit tests for a single package: ```sh npm run test:package ``` #### E2E tests ```sh PORT=8181 npm run test:e2e ``` It will run `test.sh` script which in turn, will run app, wait for it to start listening and run `npm run test:cucumber` command. You need to provide the script with valid PORT or default (8080) will be used. #### Linter ```sh npm run lint # to just show linter errors and warnings npm run lint:fix # to fix the errors and show what's left ``` ### Typescript types management **Note!** Because we're using `"noImplicitAny": true`, we are required to have a `.d.ts` file for **every** library we use. While we could set `noImplicitAny` to `false` to silence errors about missing `.d.ts` files, it is a best practice to have a `.d.ts` file for every library. 1. After installing any npm package as a dependency or dev dependency, immediately try to install the `.d.ts` file via `@types`. If it succeeds, you are done. If not, continue to next step. 2. Try to generate a `.d.ts` file with dts-gen. If it succeeds, you are done. If not, continue to next step. 3. Create a file called `.d.ts` in `types` folder. 4. Add the following code: ```ts declare module ''; ``` 5. At this point everything should compile with no errors and you can either improve the types in the `.d.ts` file by following this [guide on authoring `.d.ts` files](http://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html) or continue with no types. ## Lerna 1. To run `npm i` on all packages, and compile them, just run ```bash lerna bootstrap ``` 2. To run any command on all packages, just can use `exec` command. F.e. to compile all packages, you can run ```bash lerna exec tsc ``` 3. To clean all `node_modules` in packages, run ```bash lerna clean ``` 4. To clean all `node_modules` AND `dist` directories, run ```bash npm run clean ``` 5. To add dependency between packages, run ```bash lerna add --scope=, ``` 6. To publish to npm, run ```bash lerna publish ``` To get more command, please visit this [link](https://github.com/lerna/lerna). ================================================ FILE: docker-compose.yml ================================================ version: '2' networks: brainhub: ~ services: brainhub-framework-app: image: brainhub-framework-app build: context: . container_name: fs-brainhub-framework-app volumes: - ./:/usr/src/app ports: - 8080:8080 environment: NODE_ENV: development command: npm run start:development networks: brainhub: ~ ================================================ FILE: entrypoint.sh ================================================ #!/usr/bin/env bash set -e if [ ! -f /usr/src/app/node_modules/.lock ] && [ -f /usr/src/app/package.json ]; then npm install --progress=false touch /usr/src/app/node_modules/.lock fi exec "$@" ================================================ FILE: features/step_definitions/steps.ts ================================================ import { defineSupportCode } from 'cucumber'; import stepsSupport from '@brainhubeu/cucumber-steps'; defineSupportCode(stepsSupport); ================================================ FILE: features/step_definitions/theBestSteps.ts ================================================ import { defineSupportCode } from 'cucumber'; /* tslint:disable:only-arrow-functions ter-prefer-arrow-callback */ defineSupportCode(function({ Given, When, Then }) { Given('brainhub is the best', function(callback) { callback(); }); When('add {string} to {string}', function(number1, number2) { this.result = parseInt(number1, 10) + parseInt(number2, 10); }); Then('the result is {string}', function(result) { if (this.result !== parseInt(result, 10)) { throw new Error( `Expected result to be equal ${result} but it is ${this.result}`, ); } }); Then('the result is null', function() { if (this.result !== null) { throw new Error(`Expected result to be null but it is ${this.result}`); } }); }); ================================================ FILE: features/support/hooks/mock.ts ================================================ import { defineSupportCode } from 'cucumber'; import * as superagent from 'superagent'; import Client from '../scripts/Client'; defineSupportCode(({ Before }) => { Before(function(scenarioResult) { this.client = new Client(superagent); this.client.setHost('http://localhost:8080'); }); }); ================================================ FILE: features/support/scripts/Client.ts ================================================ import Response from './Response'; const METHOD = { DELETE: 'del', GET: 'get', PATCH: 'patch', POST: 'post', PUT: 'put', }; export default class Client { public superagent: any; public host: string; public headers: object; constructor(superagent) { this.superagent = superagent; this.headers = {}; Object.keys(METHOD).forEach((methodKey) => { const method = METHOD[methodKey.toUpperCase()]; this[methodKey.toLowerCase()] = (path, body) => { return this.createRequest(method, path, body); }; }); } public setHost(host) { this.host = host; } public createRequest(method, path, body) { const request = this.superagent[method.toLowerCase()](this.host + path); this.addRequestHeaders(request); const createdRequest = method.toLowerCase() !== 'get' ? request.send(body) : request; // return createdRequest.then( // tslint:disable:no-shadowed-variable ({ body, status }) => new Response(body, status), ({ response: { body, status } }) => new Response(body, status), ); } public addRequestHeaders(request) { Object.keys(this.headers).map((name) => { request.set(name, this.headers[name]); }); } public setHeader(name, value) { this.headers[name] = value; } } ================================================ FILE: features/support/scripts/Response.ts ================================================ export default class Response { public status: any; public body: any; constructor(body, status) { this.body = body; this.status = status; } } ================================================ FILE: features/support/world.ts ================================================ import { defineSupportCode } from 'cucumber'; /* tslint:disable:only-arrow-functions ter-prefer-arrow-callback */ defineSupportCode(function({ setWorldConstructor }) { const customWorld = function() { this.result = null; }; setWorldConstructor(customWorld); }); ================================================ FILE: features/theBest.feature ================================================ Feature: The Best As a Brainhub We want to become best in the world Scenario: Adding two numbers Given brainhub is the best When add "2" to "3" Then the result is "5" Scenario: Not adding two numbers Then the result is null ================================================ FILE: lerna.json ================================================ { "lerna": "2.9.0", "packages": [ "packages/hadron-logger", "packages/hadron-utils", "packages/hadron-error-handler", "packages/hadron-core", "packages/hadron-file-locator", "packages/hadron-json-provider", "packages/hadron-serialization", "packages/hadron-validation", "packages/hadron-typeorm", "packages/hadron-events", "packages/hadron-express", "packages/hadron-oauth", "packages/hadron-auth", "packages/hadron-demo" ], "version": "independent" } ================================================ FILE: ormconfig.json ================================================ [ { "name": "postgres", "type": "postgres", "host": "localhost", "port": 5432, "username": "postgres", "password":"mysecretpassword", "database": "_test", "synchronize": true, "logging": false, "autoSchemaSync": true, "entities": [ "../../src/entity/**/*.ts" ], "migrations": [ "../../src/migration/**/*.ts" ], "subscribers": [ "../../src/subscriber/**/*.ts" ] }, { "name": "mysql", "type": "mysql", "host": "localhost", "port": 3306, "username": "root", "password":"my-secret-pw", "database": "_test", "synchronize": true, "logging": false, "autoSchemaSync": true, "entities": [ "../../src/entity/**/*.ts" ], "migrations": [ "../../src/migration/**/*.ts" ], "subscribers": [ "../../src/subscriber/**/*.ts" ] } ] ================================================ FILE: package-test.sh ================================================ #!/usr/bin/env sh # this runs unit tests on an individual package # Usage example: ./package-test.sh oauth ./node_modules/mocha/bin/mocha -r ts-node/register packages/hadron-$1/src/__tests__/* ================================================ FILE: package.json ================================================ { "name": "brainhub-framework-app", "version": "1.0.0-alpha.1", "description": "Brainhub framework app", "main": "dist", "scripts": { "start": "NODE_ENV=production NODE_PATH=./packages/hadron-demo/dist npm start --prefix packages/hadron-demo", "start:dev": "NODE_ENV=development NODE_PATH=./packages/hadron-demo nodemon --watch 'packages/hadron-demo/**/*.ts' --watch .rebuild.ts --ignore 'packages/hadron-demo/**/*.spec.ts' --exec 'ts-node' packages/hadron-demo/index.ts", "start:test": "NODE_ENV=development NODE_PATH=./packages/hadron-demo ts-node packages/hadron-demo", "start:lerna": "nodemon --watch 'packages/**/*.ts' --exec 'npm run build && touch .rebuild.ts' -e ts", "build": "lerna bootstrap", "prestart": "npm run build", "precommit": "lint-staged", "lint": "lerna exec --bail=false -- tslint -c \\$LERNA_ROOT_PATH/tslint.json -p ./tsconfig.json --format stylish", "lint:fix": "lerna exec --bail=false -- tslint -c \\$LERNA_ROOT_PATH/tslint.json -p \\packages/$LERNA_PACKAGE_NAME/tsconfig.json --format stylish", "test": "lerna bootstrap && npm run -s test:unit && npm run -s test:e2e", "test:unit": "NODE_ENV=test NODE_PATH=src:lib mocha -r ts-node/register tools/testSetup.ts './**/__tests__/*.ts'", "test:package": "./package-test.sh", "test:unit:watch": "npm run test:unit -- --reporter min --watch-extensions ts --watch", "test:unit:coverage": "NODE_ENV=test NODE_PATH=src:lib nyc mocha -r ts-node/register tools/testSetup.ts './**/__tests__/*.ts'", "test:e2e": "./test.sh", "test:cucumber": "NODE_ENV=test ./node_modules/.bin/cucumberjs --compiler=ts:ts-node/register", "docker:build": "docker build -t brainhub-framework-app .", "format": "prettier --write \"*/**/*.ts\"", "tsc": "lerna exec tsc --parallel", "clean": "bash ./scripts/clean", "postinstall": "lerna bootstrap" }, "author": "Brainhub", "license": "MIT", "dependencies": { "@types/bunyan": "1.8.4", "body-parser": "1.18.2", "dotenv": "4.0.0", "mysql": "2.15.0", "pg": "7.4.1", "pm2": "github:Unitech/pm2#development" }, "devDependencies": { "@types/chai": "4.1.2", "@types/chai-as-promised": "7.1.0", "@types/cucumber": "4.0.1", "@types/dotenv": "4.0.2", "@types/fs-extra": "5.0.1", "@types/mocha": "2.2.48", "@types/multer": "1.3.6", "@types/node": "9.4.6", "@types/sinon": "4.1.3", "@types/sinon-chai": "2.7.29", "chai": "4.1.2", "chai-as-promised": "7.1.1", "concurrently": "3.5.1", "cucumber": "3.1.0", "@brainhubeu/cucumber-steps": "git+https://github.com/brainhubeu/cucumber-steps.git", "dirty-chai": "2.0.1", "husky": "0.14.3", "lerna": "^2.9.0", "lint-staged": "7.0.0", "mocha": "4.0.1", "nodemon": "1.12.1", "nyc": "11.6.0", "prettier": "1.11.1", "reflect-metadata": "0.1.12", "sinon": "4.1.2", "sinon-chai": "2.14.0", "superagent": "3.8.2", "ts-node": "5.0.0", "tslint": "5.9.1", "tslint-config-airbnb": "5.7.0", "tslint-config-prettier": "1.10.0", "tslint-eslint-rules": "5.1.0", "typescript": "2.7.2" }, "nyc": { "extension": [ ".ts" ] }, "lint-staged": { "*.{ts,tsx}": [ "npm run lint:fix", "git add" ], "*.{ts,js,json,css,md}": [ "prettier --write", "git add" ] }, "repository": { "type": "git", "url": "https://github.com/brainhubeu/hadron.git" }, "keywords": [ "hadron" ] } ================================================ FILE: packages/hadron-auth/LICENSE ================================================ MIT License Copyright (c) 2018 Brainhub Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/hadron-auth/README.md ================================================ ## Installation ```bash npm install @brainhubeu/hadron-auth --save ``` ## Overview **hadron-auth** provides back-end authorization layer for routes You will choose. ### Configuration with Hadron Core If You want to use **hadron-auth** with **hadron-core** You should also use **hadron-typeorm** and **hadron-express**. All You need to provide is two schemas for typeorm: * `User` (id, username, and roles many-to-many relation required) Here is the example schema: ```javascript // schemas/User const userSchema = { name: 'User', columns: { id: { primary: true, type: 'int', generated: true, }, username: { type: 'varchar', unique: true, }, passwordHash: { type: 'varchar', }, addedOn: { type: 'timestamp', }, }, relations: { roles: { target: 'Role', type: 'many-to-many', joinTable: { name: 'user_role', }, onDelete: 'CASCADE', }, }, }; module.exports = userSchema; ``` * `Role` (id and name required) Example schema: ```javascript // schemas/Role const roleSchema = { name: 'Role', columns: { id: { primary: true, type: 'int', generated: true, }, name: { type: 'varchar', unique: true, }, addedOn: { type: 'timestamp', }, }, }; module.exports = roleSchema; ``` Don't forget to add schemas to Your database config, example below: ```javascript // config/db.js const userSchema = require('../schemas/User'); const roleSchema = require('../schemas/Role'); const connection = { name: 'mysql-connection', type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'my-secret-pw', database: 'done-it', entitySchemas: [roleSchema, userSchema], synchronize: true, }; module.exports = connection, ``` Now You need to prepare Your hadron configuration file, where You can add secured routes, for example: ```javascript // index.js const config = { routes: { helloWorldRoute: { path: '/', methods: ['GET'], callback: () => 'Hello World', }, adminRoute: { path: '/admin', methods: ['GET'], callback: () => 'Hello Admin', }, userRoute: { path: '/user', methods: ['GET'], callback: () => 'Hello User', }, }, securedRoutes: [ { path: '/admin/*', methods: ['GET', 'POST', 'PUT', 'DELETE'], roles: 'Admin', }, { path: '/user/*', roles: ['Admin', 'User'], }, ], }; ``` Finally You need to add **hadron-auth** to hadron initialization method: ```javascript const hadron = require('@brainhubeu/hadron-core').default; const hadronExpress = require('@brainhubeu/hadron-express'); const hadronTypeOrm = require('@brainhubeu/hadron-typeorm'); const hadronAuth = require('@brainhubeu/hadron-auth'); const express = require('express'); const expressApp = express(); const hadronInit = async () => { const config = { routes: { helloWorldRoute: { path: '/', methods: ['GET'], callback: () => 'Hello World', }, adminRoute: { path: '/admin', methods: ['GET'], callback: () => 'Hello Admin', }, userRoute: { path: '/user', methods: ['GET'], callback: () => 'Hello User', }, }, securedRoutes: [ { path: '/admin/*', methods: ['GET', 'POST', 'PUT', 'DELETE'], roles: 'Admin', }, { path: '/user/*', roles: ['Admin', 'User'], }, ], }; const container = await hadron( expressApp, [hadronAuth, hadronExpress, hadronTypeOrm], config, ); }; ``` --- Warning, You should pass hadronAuth as first to hadron packages array. --- Now Your routes are secured, by default, **hadron-auth** authorize user by **JWT Token**, passed as `Authorization` header. ### Creating custom auth middleware You can pass Your own function in hadron configuration to check if a user is authorized to the secured route. Here is the skeleton for the authorization middleware: ```javascript const authorizationMiddleware = (container) => { return (req, res, next) => {}; }; ``` **hadron-auth** provides `isAllowed` function, to check if a user is allowed to specified route: ```javascript isAllowed(path, method, user, allRoles); ``` Where: * `path` - path to secured route, for example /api/admin/1 * `method` - HTTP method * `user` - User object, which need to contain roles * `allRoles` - All roles stored in database (only role names) Here is an example authorization middleware: ```javascript const jwt = require('jsonwebtoken'); const { isRouteSecure, isAllowed } = require('@brainhubeu/hadron-auth'); const errorResponse = { message: 'Unauthorized', }; const expressMiddlewareAuthorization = (container) => { return async (req, res, next) => { try { if (!isRouteSecure(req.path)) { return next(); } const userRepository = container.take('userRepository'); const roleRepository = container.take('roleRepository'); const token = req.headers.authorization; const decoded: any = jwt.decode(token); const user = await userRepository.findOne({ where: { id: decoded.id }, relations: ['roles'], }); if (!user) { return res.status(403).json({ error: errorResponse }); } const allRoles = await roleRepository.find(); if ( isAllowed(req.path, req.method, user, allRoles.map((role) => role.name)) ) { return next(); } return res.status(403).json({ error: errorResponse }); } catch (error) { return res.status(403).json({ error: errorResponse }); } }; }; module.exports = expressMiddlewareAuthorization; ``` To use it, You need to pass an expressMiddlewareAuthorization function as `authorizationMiddleware` key in hadron config. ```javascript const config = { authorizationMiddleware: YourCustomFunction, }; ``` ### Usage: ```javascript const securedRoutes = [ { path: '/api/**', methods: ['GET'], roles: ['Admin', 'User'], }, { path: '/api/**', methods: ['POST', 'PUT', 'DELETE'], roles: 'Admin', }, { path: '/admin/*', roles: 'Admin', }, { path: 'product/info', methods: ['GET'], roles: [['Admin', 'User'], 'Manager'], }, ]; ``` * `Path` - here we can specify the route path we want to secure, we can use a static path like `/api/admin/tasks` or by pattern: * `/api/admin/*` - route after `/api/admin/` is secured, for example `/api/admin/tasks` - is secured, but `/api/admin/tasks/5` - will be not secured * `/api/admin/**` - every route after `/api/admin` is secured * `methods` - an array of strings, where You can pass role names, if You will not provide any role, then the route is secured and user with **ANY** role can access this if a user does not have any role he will be unauthorized. * `roles` - here You can pass single role name, an array of role names or array of arrays of strings, which add some logic functionality, for example, if we declare: ```javascript roles[(['Admin', 'User'], 'Manager')]; ``` The user needs Admin **AND** User **OR** Manager role to access the route. ================================================ FILE: packages/hadron-auth/index.ts ================================================ import { IUser, IRole } from './src/hierarchyProvider'; import * as bcrypt from './src/password/bcrypt/bcrypt'; import * as HadronAuth from './src/HadronAuth'; export const register = (container: any, config: any) => { HadronAuth.register(container, config); }; export default HadronAuth; export { IUser, IRole, bcrypt }; ================================================ FILE: packages/hadron-auth/package.json ================================================ { "name": "@brainhubeu/hadron-auth", "version": "0.0.2", "description": "Security package for hadron", "main": "dist/index.js", "files": [ "dist", "LICENSE" ], "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "prepare": "tsc" }, "keywords": [ "security", "hadron" ], "author": "Brainhub", "license": "MIT", "dependencies": { "@types/bcrypt": "^2.0.0", "@types/jsonwebtoken": "^7.2.7", "bcrypt": "2.0.0", "glob-to-regexp": "^0.4.0", "jsonwebtoken": "8.3.0" }, "publishConfig": { "access": "public" } } ================================================ FILE: packages/hadron-auth/src/HadronAuth.ts ================================================ import { IRoute, IMethod } from './IRoute'; import urlGlob, { convertToPattern } from './helpers/urlGlob'; import { IUser } from '..'; import { isUserGranted } from './hierarchyProvider'; import expressMiddlewareAuthorization from './providers/expressMiddlewareAuthorization'; let routes: IRoute[] = []; export interface ISecuredRoute { path: string; methods?: string[]; roles: string | Array; } export const getExistsingRoute = (path: string, routes: IRoute[]): IRoute => { for (const route of routes) { if (route.path === path) { return route; } } return null; }; export const getRoleArray = (roles: string | Array) => { const arr: any[] = []; if (typeof roles === 'string') { arr.push(roles); } else { roles.forEach((role) => arr.push(role)); } return [...new Set(arr)]; }; export const getMethodsForExistsingRoute = ( existingRoute: IRoute, methods: string[], roles: string | Array, ): IMethod[] => { const newMethods: IMethod[] = methods.map((method) => ({ allowedRoles: getRoleArray(roles), name: method, })); const existingMethods = existingRoute.methods.filter( (method) => newMethods.map((el) => el.name).indexOf(method.name) >= 0, ); const nonExistingMethods = newMethods.filter( (method) => existingRoute.methods.map((el) => el.name).indexOf(method.name) === -1, ); existingMethods.forEach((method) => { method.allowedRoles = [ ...new Set(method.allowedRoles.concat(getRoleArray(roles))), ]; }); let methodsFromRoute = existingRoute.methods.filter( (method) => existingMethods.map((el) => el.name).indexOf(method.name) === -1, ); methodsFromRoute = methodsFromRoute.concat(existingMethods); return [...methodsFromRoute, ...nonExistingMethods]; }; export const createNewRoute = ( path: string, methods: string[] = [], roles: string | Array, ) => { const methodsForRoute: IMethod[] = []; if (methods.length > 0) { methods = [...new Set(methods)]; methods.forEach((methodName) => { methodsForRoute.push({ name: methodName, allowedRoles: getRoleArray(roles), }); }); } else { methodsForRoute.push({ name: '*', allowedRoles: getRoleArray(roles), }); } const route: IRoute = { path: convertToPattern(path), methods: methodsForRoute, }; return route; }; export const initRoutes = (securedRoutes: ISecuredRoute[]): IRoute[] => { const routes: IRoute[] = []; securedRoutes.forEach((route) => { const existingRoute = getExistsingRoute( convertToPattern(route.path), routes, ); if (existingRoute) { existingRoute.methods = getMethodsForExistsingRoute( existingRoute, route.methods, route.roles, ); } else { routes.push(createNewRoute(route.path, route.methods, route.roles)); } }); return routes; }; export const register = (container: any, config: any) => { if (config.authSecret) { container.register('authSecret', config.authSecret); } routes = initRoutes(config.securedRoutes || []); const server = container.take('server'); server.use( config.authorizationMiddleware ? config.authorizationMiddleware(container) : expressMiddlewareAuthorization(container), ); }; export const getRouteFromPath = (path: string, routes: IRoute[]): IRoute => { const route = routes.filter((r) => urlGlob(r.path, path)); if (route.length === 0) return null; return route[0]; }; export const isRouteNotSecure = (path: string) => { console.warn("HadronAuth: isRouteNotSecure is being deprecated. Use isRouteSecure instead."); return getRouteFromPath(path, routes) === null; } export const isRouteSecure = (path: string) => getRouteFromPath(path, routes) !== null; export const isAllowed = ( path: string, allowedMethod: string, user: IUser, allRoles: string[], ): boolean => { try { const route = getRouteFromPath(path, routes); let isGranted = false; route.methods.forEach((method) => { if ( method.name === '*' || method.name.toLowerCase() === allowedMethod.toLowerCase() ) { if (method.allowedRoles.includes('*') && user.roles.length > 0) { isGranted = true; } else { isGranted = isUserGranted(user, method.allowedRoles, { ALL: allRoles, }); } } }); return isGranted; } catch (error) { throw new Error('Unauthorized'); } }; ================================================ FILE: packages/hadron-auth/src/IRoute.ts ================================================ export interface IRoute { path: string; methods: IMethod[]; } export interface IMethod { name: string; allowedRoles: Array; } ================================================ FILE: packages/hadron-auth/src/ISecurityOptions.ts ================================================ import { IRolesMap } from './hierarchyProvider'; import IHashMethod from './password/IHashMethod'; export default interface ISecurityOptions { roles: IRolesMap | string[]; hash?: { method: IHashMethod | string; options: any; }; }; ================================================ FILE: packages/hadron-auth/src/__tests__/HadronAuth.ts ================================================ import { expect } from 'chai'; import { initRoutes, ISecuredRoute, getRouteFromPath, getExistsingRoute, createNewRoute, getMethodsForExistsingRoute, } from '../HadronAuth'; import { convertToPattern } from '../helpers/urlGlob'; describe('Hadron Authorization module', () => { it('initRoutes should return array of prepared IRoute from ISecuredRoute array', () => { const securedRoutes: ISecuredRoute[] = [ { path: '/admin/**', methods: ['POST', 'PUT'], roles: ['Admin', 'User'], }, ]; const routes = initRoutes(securedRoutes); expect(routes).to.be.instanceOf(Array); expect(Object.keys(routes[0])).to.be.deep.equal(['path', 'methods']); }); it('initRoutes should join two same paths with different methods/roles', () => { const securedRoutes: ISecuredRoute[] = [ { path: '/admin/**', methods: ['POST', 'PUT'], roles: 'Admin', }, { path: '/admin/**', methods: ['GET'], roles: 'User', }, ]; const routes = initRoutes(securedRoutes); expect(routes.length).to.be.equal(1); expect(Object.keys(routes[0])).to.be.deep.equal(['path', 'methods']); }); it('initRoutes should convert path from securedRoute to regex pattern', () => { const securedRoutes: ISecuredRoute[] = [ { path: '/admin/*', methods: ['POST'], roles: 'Admin', }, ]; const routes = initRoutes(securedRoutes); const matcher = new RegExp(routes[0].path); const testPath = '/admin/1'; expect(matcher.test('admin/1')).to.be.equal(true); }); describe('getRouteFromPath', () => { const routes = initRoutes([ { path: '/admin/*', methods: ['POST'], roles: 'Admin', }, { path: '/user/*', methods: ['POST'], roles: 'Admin', }, ]); it('getRouteFromPath should return IRoute if path is already exists by regex in routes', () => { expect(getRouteFromPath('/admin/1', routes)).to.be.an('object'); }); it('getRouteFromPath should return null if path does not exists by regex in routes', () => { expect(getRouteFromPath('/qwe', routes)).to.be.equal(null); }); }); describe('getExistingRoute', () => { const routes = initRoutes([ { path: '/admin/*', methods: ['POST'], roles: 'Admin', }, { path: '/user/*', methods: ['POST'], roles: 'Admin', }, ]); it('getExistingRoute should return route if route exists in array', () => { expect( getExistsingRoute(convertToPattern('/admin/*'), routes), ).to.be.equal(routes[0]); }); it('getExistingRoute should return null if route does not exists in array', () => { expect( getExistsingRoute(convertToPattern('/guest/*'), routes), ).to.be.equal(null); }); }); describe('createNewRoute', () => { const path = '/admin/**'; const methods: string[] = []; const roles = 'Admin'; const route = createNewRoute(path, methods, roles); it('createNewRoute should create IRoute from path, methods and roles', () => { expect(Object.keys(route)).to.be.deep.equal(['path', 'methods']); }); it('createNewRoute should create regex pattern from path string', () => { expect(route.path).to.be.equal(convertToPattern('/admin/**')); }); it('createNewRoute should push "*" to methods array if array is empty', () => { expect(route.methods[0].name).to.be.equal('*'); }); it('createNewRoute should create IMethod with name and roles in route object', () => { expect(Object.keys(route.methods[0])).to.be.deep.equal([ 'name', 'allowedRoles', ]); }); }); describe('getMethodsForExistingRoute', () => { const path = '/admin/**'; const methods: string[] = ['GET']; const roles = 'Admin'; const route = createNewRoute(path, methods, roles); it('getMethodsForExistsinRoute should push new methods to existing route', () => { const newMethods = getMethodsForExistsingRoute( route, ['POST', 'PUT'], ['User', 'Admin', 'Guest'], ); expect(newMethods.length).to.be.equal(3); }); it('getMethodsForExistsinRoute should push new roles to existing method', () => { const newMethods = getMethodsForExistsingRoute( route, ['GET'], ['User', 'Guest'], ); expect(newMethods.length).to.be.equal(1); }); }); }); ================================================ FILE: packages/hadron-auth/src/__tests__/hierarchyProvider.ts ================================================ import hierarchyProvider, { fillMissingRoles, checkRole, checkRoles, getDeeperRoles, excludeRoles, } from '../hierarchyProvider'; import { expect } from 'chai'; describe('hierarchyProvider', () => { const basicHierarchy = { ADMIN: ['USER', 'MANAGER'], MANAGER: ['USER'], GUEST: [], }; describe('fillMissingRoles', () => { it('should add missing roles from hierarchy', () => { const roles = { ROLE1: ['ROLE2', 'ROLE3'], ROLE2: ['ROLE4'], }; expect(fillMissingRoles(roles)).to.contain.keys([ 'ROLE1', 'ROLE2', 'ROLE3', 'ROLE4', ]); }); it('should keep hierarchy of previously mentioned role', () => { const roles = { ROLE1: ['ROLE2', 'ROLE3'], ROLE2: ['ROLE4'], }; expect(fillMissingRoles(roles).ROLE2).to.eql(['ROLE4']); }); it('should handle array as roles', () => { const roles = ['ROLE1', 'ROLE2']; expect(fillMissingRoles(roles)).to.contain.keys(['ROLE1', 'ROLE2']); }); }); describe('checkRole', () => { it('should return true if role is exactly the same as requested', () => expect(checkRole(['ADMIN'], 'ADMIN', basicHierarchy)).to.be.eql(true)); it('should return false if role is not exactly the same and does not contain it in hierarchy', () => expect(checkRole(['USER'], 'MANAGER', basicHierarchy)).to.be.eql(false)); it('should return true if role is not exactly the same but does contain it in hierarchy', () => expect(checkRole(['MANAGER'], 'USER', basicHierarchy)).to.be.eql(true)); it('should return true if one of roles is matching', () => expect( checkRole(['MANAGER', 'USER'], 'MANAGER', basicHierarchy), ).to.be.eql(true)); it('should return false if none of roles is matching', () => expect(checkRole(['MANAGER', 'USER'], 'ADMIN', basicHierarchy)).to.be.eql( false, )); it('should return true if role is in hierarchy of user roles', () => expect(checkRole(['ADMIN', 'USER'], 'MANAGER', basicHierarchy)).to.be.eql( true, )); it('should return true if role is in deeper hierarchy of user roles', () => { const roles = { ...basicHierarchy, MANAGER: ['TESTUSER', 'USER'], }; return expect(checkRole(['ADMIN'], 'TESTUSER', roles)).to.be.eql(true); }); it('should handle recurrent hierarchy', () => { const roles = { ADMIN: ['MANAGER'], MANAGER: ['ADMIN'], }; return expect(checkRole(['ADMIN'], 'TESTUSER', roles)).to.be.eql(false); }); it("should return false, if role doesn't exists", () => expect(checkRole(['ADMIN'], 'UNEXISTING_ROLE', basicHierarchy)).to.be.eql( false, )); }); describe('checkRoles', () => { it('should return true if role is one of given', () => expect( checkRoles(['MANAGER'], ['MANAGER', 'ADMIN'], basicHierarchy), ).to.be.eql(true)); it('should return false, if none of roles is matching given one', () => expect( checkRoles(['USER'], ['MANAGER', 'ADMIN'], basicHierarchy), ).to.be.eql(false)); it('should return true if user has both roles', () => expect( checkRoles( ['MANAGER', 'ADMIN'], ['MANAGER', 'ADMIN'], basicHierarchy, true, ), ).to.be.eql(true)); it("should return false if user doesn't have one of roles", () => expect( checkRoles(['MANAGER'], ['MANAGER', 'ADMIN'], basicHierarchy, true), ).to.be.eql(false)); it('should return true if role contains both required roles', () => expect( checkRoles(['ADMIN'], ['MANAGER', 'USER'], basicHierarchy, true), ).to.be.eql(true)); }); describe('getDeeperRoles', () => { it('should return roles that are related to given one', () => expect(getDeeperRoles(['ADMIN'], basicHierarchy)).to.has.members([ 'MANAGER', 'USER', ])); it('should return roles that are related to all given roles', () => { const hierarchy = { ROLE1: ['ROLE2', 'ROLE3'], ROLE4: ['ROLE5'], }; return expect( getDeeperRoles(['ROLE1', 'ROLE4'], hierarchy), ).to.has.members(['ROLE2', 'ROLE3', 'ROLE5']); }); it('should return roles that are related to all given roles distinctly', () => { const hierarchy = { ROLE1: ['ROLE2', 'ROLE3'], ROLE4: ['ROLE5', 'ROLE3'], }; return expect( getDeeperRoles(['ROLE1', 'ROLE4'], hierarchy), ).to.has.members(['ROLE2', 'ROLE3', 'ROLE5']); }); }); describe('excludeRoles', () => { it('should remove given role from list', () => expect(excludeRoles(['ADMIN'], basicHierarchy)).to.contain.keys([ 'MANAGER', 'GUEST', ])); it('should remove given roles from list', () => expect( excludeRoles(['ADMIN', 'MANAGER'], basicHierarchy), ).to.contain.keys(['GUEST'])); }); describe('isGranted', () => { const { isGranted } = hierarchyProvider(basicHierarchy); it('should pass if single matching role has been provided', () => expect(isGranted(['ADMIN'], 'ADMIN')).to.be.eql(true)); it('should fail if single not matching role has been provided', () => expect(isGranted(['ADMIN'], 'GUEST')).to.be.eql(false)); it('should pass if array of roles has been provided, with single matching one', () => expect(isGranted(['ADMIN'], ['ADMIN', 'GUEST'])).to.be.eql(true)); it('should fail if array of roles has been provided, with none that matches', () => expect(isGranted(['MANAGER'], ['ADMIN', 'GUEST'])).to.be.eql(false)); it('should pass if array of arrays of roles has been provided and all of them are matching', () => expect(isGranted(['ADMIN', 'GUEST'], [['ADMIN', 'GUEST']])).to.be.eql( true, )); it('should fail if array of arrays of roles has been provided and one of them is not matching', () => expect(isGranted(['ADMIN'], [['ADMIN', 'GUEST']])).to.be.eql(false)); it('should pass if array of arrays and single role has been provided and one of them are matching', () => expect(isGranted(['MANAGER'], [['ADMIN', 'GUEST'], 'MANAGER'])).to.be.eql( true, )); it('should fail if array of arrays of roles has been provided and none of them are matching', () => expect( isGranted(['MANAGER'], [['ADMIN', 'GUEST'], ['MANAGER', 'GUEST']]), ).to.be.eql(false)); it('should pass if array of arrays of roles has been provided and one of them are matching', () => expect( isGranted( ['MANAGER', 'GUEST'], [['ADMIN', 'GUEST'], ['MANAGER', 'GUEST']], ), ).to.be.eql(true)); }); }); ================================================ FILE: packages/hadron-auth/src/__tests__/urlGlob.ts ================================================ import { expect } from 'chai'; import urlGlob from '../helpers/urlGlob'; describe('Glob URL pattern', () => { it('should return true if URL: /api/admin/qwe is valid for PATTERN: /api/admin/*', () => { expect(urlGlob('/api/admin/*', '/api/admin/qwe')).to.be.equal(true); }); it('should return false if URL: /api/adm/qwe is invalid for PATTERN: /api/admin/*', () => { expect(urlGlob('/api/admin/*', '/api/adm/qwe')).to.be.equal(false); }); it('should return false if URL: /api/admin/qwe/1 is invalid for PATTERN: /api/admin/*', () => { expect(urlGlob('/api/admin/*', '/api/adm/qwe')).to.be.equal(false); }); it('should return true if URL: /api/admin/tasks/1 is valid for PATTERN: /api/admin/**', () => { expect(urlGlob('/api/admin/**', '/api/admin/tasks/1')).to.be.equal(true); }); it('should return true if URL: /api/something/admin/more/user/in/manager/string for PATTERN: /api/**/admin/**/manager/**', () => { expect( urlGlob( '/api/**/admin/**/user/**/manager/**', '/api/something/admin/more/user/in/manager/string', ), ).to.be.equal(true); }); it('should return false if URL: /api/something/admin/more/user/in/mage/string for PATTERN: /api/**/admin/**/manager/**', () => { expect( urlGlob( '/api/**/admin/**/user/**/manager/**', '/api/something/admin/more/user/in/mage/string', ), ).to.be.equal(false); }); it('should return false if URL: /api/something/admin/more is invalid for PATTERNL /api/**/user/**', () => { expect(urlGlob('/api/**/admin/**', '/api/something/user/more')).to.be.equal( false, ); }); it('should return true if URL: /api/admin is valid for strict PATTERN /api/admin', () => { expect(urlGlob('/api/admin', '/api/admin')).to.be.equal(true); }); it('should return false if URL: /api/adm or /api.admin/1 is invalid for strict PATTERN /api/admin', () => { expect(urlGlob('/api/admin', '/api/adm')).to.be.equal(false); expect(urlGlob('/api/admin', '/api/admin/1')).to.be.equal(false); }); }); ================================================ FILE: packages/hadron-auth/src/constants.ts ================================================ export const CONTAINER_NAME = 'isGranted'; ================================================ FILE: packages/hadron-auth/src/declarations.d.ts ================================================ declare module 'glob-to-regexp'; ================================================ FILE: packages/hadron-auth/src/helpers/flattenDeep.ts ================================================ const flattenDeep = (arr: any[]): any[] => Array.isArray(arr) ? arr.reduce((a, b) => [...flattenDeep(a), ...flattenDeep(b)], []) : [arr]; export default flattenDeep; ================================================ FILE: packages/hadron-auth/src/helpers/urlGlob.ts ================================================ const countStars = (input: string) => { try { return input.match(/\*\*/g).length; } catch (error) { return 0; } }; export const convertToPattern = (url: string): string => { const regexp = url[0] === '/' ? url.substring(1) : url; if (url.endsWith('/*') && countStars(url) === 0) { return `^/?${regexp.replace(/\/\*/g, '($|/$|/[^/]*$)')}$`; } if (url.endsWith('/**') && countStars(url) === 1) { return `^/?${regexp.replace(/\/\*\*/g, '($|/.*$)')}$`; } if (countStars(url) === 0) { return `^/?${url}$`; } return convertToPattern(regexp.replace('**', '[^/]*')); }; const urlGlob = (pattern: string, input: string): boolean => { const re = new RegExp(convertToPattern(pattern)); return re.test(input); }; export default urlGlob; ================================================ FILE: packages/hadron-auth/src/hierarchyProvider.ts ================================================ export interface IRolesMap { [s: string]: string[]; } export interface IRole { id: number | string; name: string; } export interface IUser { id: number | string; username: string; passwordHash: string; roles: IRole[]; } /** * Function adds roles from dependency of other roles, to make sure that all roles are available * @param roles available roles with all "dependent" ones * @returns {IRolesMap} */ export function fillMissingRoles(roles: IRolesMap | string[]): IRolesMap { if (roles instanceof Array) { return roles.reduce( (accumulator: IRolesMap, role: string) => ({ ...accumulator, [role]: [], }), {}, ); } return Object.entries(roles).reduce( (accumulator: IRolesMap, [key, value]: [string, string[]]) => { accumulator[key] = value; value.forEach((role: string) => { if (!accumulator[role]) { accumulator[role] = []; } }); return accumulator; }, {} as IRolesMap, ); } /** * Get array of all roles below in hierarchy distinctly * @param userRoles * @param availableRoles * @returns {string[]} */ export function getDeeperRoles(userRoles: string[], availableRoles: IRolesMap) { return userRoles .filter((role: string) => !!availableRoles[role]) .reduce( (accumulator: string[], role: string) => [ ...accumulator, ...availableRoles[role].filter( (roleToAdd) => !accumulator.includes(roleToAdd), ), ], [], ); } /** * Returns array of all given roles, without ones given in first parameter * @param userRoles * @param availableRoles * @returns {string[]} */ export function excludeRoles(userRoles: string[], availableRoles: IRolesMap) { return Object.entries(availableRoles) .filter(([key, value]: [string, any]) => userRoles.indexOf(key) < 0) .reduce( (accumulator: object, [key, value]: [string, any]) => ({ ...accumulator, [key]: value, }), {}, ); } /** * Checks if user role contains required role * @param userRoles * @param requiredRole * @param availableRoles * @returns {boolean} */ export function checkRole( userRoles: string[], requiredRole: string, availableRoles: IRolesMap, ): boolean { if (userRoles.length <= 0) { return false; } if (userRoles.indexOf(requiredRole) >= 0) { return true; } return checkRole( getDeeperRoles(userRoles, availableRoles), requiredRole, // excludes currently checked roles to avoid endless recurrency excludeRoles(userRoles, availableRoles), ); } /** * Checks list of roles * * @param userRoles * @param requiredRoles * @param availableRoles * @param exact specifies if user needs all roles from requiredRoles (true), or only one of them (false) * @return {boolean} */ export function checkRoles( userRoles: string[], requiredRoles: string[], availableRoles: IRolesMap, exact = false, ): boolean { if (userRoles.length <= 0) { return false; } return requiredRoles .map( (role) => typeof role === 'object' ? checkRoles(userRoles, role, availableRoles, true) : checkRole(userRoles, role, availableRoles), ) .reduce( (accumulator, currentValue) => exact ? accumulator && currentValue : accumulator || currentValue, ); } /** * Returns true if given roles are matching expected roles in hierarchy * @param {IUser} user * @param roles * @param allRoles * @returns {boolean} */ export function isGranted( userRoles: string[], roles: any, allRoles: IRolesMap, ): boolean { if (typeof roles === 'string') { return checkRole(userRoles, roles, allRoles); } if (typeof roles === 'object') { return checkRoles(userRoles, roles, allRoles); } throw new Error('Unknown role type'); } /** * Returns true if user has matching role in hierarchy * @param {IUser} user * @param roles * @param allRoles * @returns {boolean} */ export function isUserGranted( user: IUser, roles: any, allRoles: IRolesMap, ): boolean { return isGranted(user.roles.map((role) => role.name), roles, allRoles); } /** * Provider for hierarchy manager * @param rolesHierarchy * @returns {function} */ export default function hierarchyProvider( rolesHierarchy: IRolesMap | string[], ) { const fullRoles: IRolesMap = fillMissingRoles(rolesHierarchy); return { isGranted: (userRoles: string[], roles: any) => isGranted(userRoles, roles, fullRoles), isUserGranted: (user: IUser, roles: any) => isUserGranted(user, roles, fullRoles), }; } ================================================ FILE: packages/hadron-auth/src/password/IHashMethod.ts ================================================ export default interface IHashMethod { hash(password: string, salt?: string, options?: object): Promise; compare( userPassword: string, hashedPassword: string, salt?: string, options?: object, ): Promise; }; ================================================ FILE: packages/hadron-auth/src/password/__tests__/hashMethodProvider.ts ================================================ import hashMethodProvider from '../hashMethodProvider'; import { expect } from 'chai'; import * as sinon from 'sinon'; import ISecurityOptions from '../../ISecurityOptions'; import IHashMethod from '../IHashMethod'; describe('hashMethodProvider', () => { const defaultOptions = { roles: [], } as ISecurityOptions; it('should return bcrypt on default', () => { const bcryptSpy = sinon.spy(); hashMethodProvider(defaultOptions, { bcrypt: bcryptSpy }); return expect(bcryptSpy.calledOnce).to.be.equal(true); }); it('should return method, which name was defined in config', () => { const testSpy = sinon.spy(); const options = { ...defaultOptions, hash: { method: 'testMethod', }, } as ISecurityOptions; hashMethodProvider(options, { testMethod: testSpy }); return expect(testSpy.calledOnce).to.be.equal(true); }); it("should return default method, if method name from config doesn't exists", () => { const bcryptSpy = sinon.spy(); const options = { ...defaultOptions, hash: { method: 'testMethod', }, } as ISecurityOptions; hashMethodProvider(options, { bcrypt: bcryptSpy }); return expect(bcryptSpy.calledOnce).to.be.equal(true); }); it('should return method defined in config', () => { const hashStub = sinon.spy(); const method = { hash: hashStub, compare: (userPassword: string, hashedPassword: string) => Promise.resolve(true), } as IHashMethod; const options = { ...defaultOptions, hash: { method, }, } as ISecurityOptions; hashMethodProvider(options).hash('smth'); return expect(hashStub.calledOnce).to.be.equal(true); }); it('should return default method, if hash method defined in config is incorrect', () => { const bcryptSpy = sinon.spy(); const method = { hassh: (userPassword: string, hashedPassword: string) => Promise.resolve(true), compaare: (userPassword: string, hashedPassword: string) => Promise.resolve(true), }; const options = { ...defaultOptions, hash: { method, }, }; hashMethodProvider(options as any, { bcrypt: bcryptSpy }); return expect(bcryptSpy.calledOnce).to.be.equal(true); }); it('should pass options to hash method call of hashMethod', () => { const hashStub = sinon.spy(); const method = { hash: hashStub, compare: (userPassword: string, hashedPassword: string) => Promise.resolve(true), } as IHashMethod; const options = { ...defaultOptions, hash: { method, options: { lorem: 'ipsum' }, }, } as ISecurityOptions; hashMethodProvider(options).hash('smth'); return expect( hashStub.calledWith('smth', undefined, { lorem: 'ipsum' }), ).to.be.equal(true); }); it('should pass options to compare method call of hashMethod', () => { const compareStub = sinon.spy(); const method = { hash: (userPassword: string) => Promise.resolve('password'), compare: compareStub, } as IHashMethod; const options = { ...defaultOptions, hash: { method, options: { lorem: 'ipsum' }, }, } as ISecurityOptions; hashMethodProvider(options).compare('simple', 'hashed'); return expect( compareStub.calledWith('simple', 'hashed', undefined, { lorem: 'ipsum' }), ).to.be.equal(true); }); }); ================================================ FILE: packages/hadron-auth/src/password/bcrypt/IBcryptOptions.ts ================================================ export default interface IBcryptOptions { saltRounds?: number; }; ================================================ FILE: packages/hadron-auth/src/password/bcrypt/__tests__/bcrypt.ts ================================================ import { hash, compare } from '../bcrypt'; import * as bcrypt from 'bcrypt'; import { expect } from 'chai'; import * as sinon from 'sinon'; describe('bcrypt', () => { describe('hash', () => { let hashStub: any = null; let genSaltStub: any = null; before(() => { hashStub = sinon.stub(bcrypt, 'hash'); genSaltStub = sinon.stub(bcrypt, 'genSalt'); }); beforeEach(() => { hashStub.reset(); genSaltStub.reset(); hashStub.returns(Promise.resolve('h45h')); genSaltStub.returns(Promise.resolve('54lt')); }); after(() => { hashStub.restore(); genSaltStub.restore(); }); it('should run bcrypt hash method, if salt given', () => { const password = 'loremIpsum'; const salt = 'd0n7-b3-s0-s4l7y'; return hash(password, salt).then(() => expect(hashStub.calledWithExactly(password, salt)).to.be.equal(true), ); }); it('should generate salt if none given', () => { const password = 'loremIpsum'; return hash(password, null).then(() => expect(hashStub.calledWithExactly(password, '54lt')).to.be.equal(true), ); }); it('should run genSalt method of bcrypt, if no salt was passed', () => { const password = 'loremIpsum'; return hash(password, null).then(() => expect(genSaltStub.calledWith()).to.be.equal(true), ); }); it('should run genSalt method of bcrypt, if no salt was passed, with saltRound property from options', () => { const password = 'loremIpsum'; const options = { saltRounds: 12, }; return hash(password, null, options).then(() => expect(genSaltStub.calledWithExactly(12)).to.be.equal(true), ); }); }); describe('compare', () => { let compareStub: any = null; before(() => { compareStub = sinon.stub(bcrypt, 'compare'); }); beforeEach(() => { compareStub.reset(); compareStub.returns(Promise.resolve(true)); }); after(() => { compareStub.restore(); }); it('should run compare method', () => compare('loremIpsum', 'loremIpsum1').then(() => expect( compareStub.calledWithExactly('loremIpsum', 'loremIpsum1'), ).to.be.equal(true), )); it('should resolves with true if passwords are matching', () => expect(compare('loremIpsum', 'loremIpsum1')).to.be.eventually.equal( true, )); it('should resolves with false if passwords are not matching', () => { compareStub.returns(Promise.resolve(false)); return expect( compare('loremIpsum', 'loremIpsum1'), ).to.be.eventually.equal(false); }); }); }); ================================================ FILE: packages/hadron-auth/src/password/bcrypt/bcrypt.ts ================================================ import * as bcrypt from 'bcrypt'; import IHashMethod from '../IHashMethod'; import IBcryptOptions from './IBcryptOptions'; export function hash( password: string, salt?: string, options: IBcryptOptions = {}, ): Promise { if (!salt) { return bcrypt .genSalt(options.saltRounds) .then((salt) => bcrypt.hash(password, salt)); } return bcrypt.hash(password, salt); } export function compare( userPassword: string, hashedPassword: string, options?: IBcryptOptions, ): Promise { return bcrypt.compare(userPassword, hashedPassword); } export default function bcryptProvider(options: IBcryptOptions): IHashMethod { return { hash: (password: string, salt?: string) => hash(password, salt, options), compare: (userPassword: string, hashedPassword: string) => compare(userPassword, hashedPassword, options), } as IHashMethod; } ================================================ FILE: packages/hadron-auth/src/password/hashMethodProvider.ts ================================================ import bcrypt from './bcrypt/bcrypt'; import ISecurityOptions from '../ISecurityOptions'; import IHashMethod from './IHashMethod'; export interface IHashProviderMap { [s: string]: (options?: object) => IHashMethod; } const availableMethod: IHashProviderMap = { bcrypt, }; /** * Function checks if given object is implementing IHashMethod interface * @param {IHashMethod | string} hashMethod * @returns {boolean} */ function isHashMethod( hashMethod: IHashMethod | string, ): hashMethod is IHashMethod { return ( (hashMethod as IHashMethod).compare !== undefined && (hashMethod as IHashMethod).hash !== undefined ); } /** * Funtion exctracts hashing method from config. Bcrypt on default. * @param config * @param methods */ export default function hashMethodProvider( config: ISecurityOptions, methods?: IHashProviderMap, ) { const allMethods: IHashProviderMap = { ...availableMethod, ...methods }; if (config.hash) { const { method, options } = config.hash; if (typeof method === 'string' && allMethods[method]) { return allMethods[method](options); } if (isHashMethod(method)) { return { hash: (password: string, salt?: string) => method.hash(password, salt, options), compare: ( userPassword: string, hashedPassword: string, salt?: string, ) => method.compare(userPassword, hashedPassword, salt, options), }; } } return allMethods.bcrypt(); } ================================================ FILE: packages/hadron-auth/src/providers/expressMiddlewareAuthorization.ts ================================================ import * as jwt from 'jsonwebtoken'; import { isRouteSecure, isAllowed } from '../HadronAuth'; const errorResponse = { message: 'Unauthorized', }; const expressMiddlewareAuthorization = (container: any) => { return async (req: any, res: any, next: any) => { try { if (!isRouteSecure(req.path)) { return next(); } const userRepository = container.take('userRepository'); const roleRepository = container.take('roleRepository'); const token = req.headers.authorization.split(' ')[1]; const secret = container.take('authSecret'); const id: any = jwt.verify(token, secret); const user = await userRepository.findOne({ where: { id }, relations: ['roles'], }); if (!user) { return res.status(401).json({ error: errorResponse }); } const allRoles = await roleRepository.find(); if ( // @ts-ignore isAllowed(req.path, req.method, user, allRoles.map((role) => role.name)) ) { return next(); } return res.status(401).json({ error: errorResponse }); } catch (error) { return res.status(401).json({ error: errorResponse }); } }; }; export default expressMiddlewareAuthorization; ================================================ FILE: packages/hadron-auth/tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "target": "es6", "noImplicitAny": true, "noEmitOnError": true, "moduleResolution": "node", "sourceMap": true, "outDir": "dist", "baseUrl": ".", "allowJs": true, "paths": { "*": ["./*"] }, "emitDecoratorMetadata": true, "experimentalDecorators": true, "inlineSources": true, "lib": ["es2017", "dom"] }, "include": ["src/**/*.*", "index.ts", "LICENSE"], "exclude": ["node_modules", "src/__tests__/**"] } ================================================ FILE: packages/hadron-core/LICENSE ================================================ MIT License Copyright (c) 2018 Brainhub Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/hadron-core/README.md ================================================ ## Installation * Install Node.js. We recommend using the latest version, installation details on [nodejs.org](https://nodejs.org) * Install following modules from npm: ```bash npm install @brainhubeu/hadron-core @brainhubeu/hadron-express express --save ``` ## Hello world app Let's start with traditional Hello World app. It will give you a quick grasp of the framework. ```javascript const hadron = require('@brainhubeu/hadron-core').default; const express = require('express'); const port = 8080; const expressApp = express(); const config = { routes: { helloWorldRoute: { path: '/', callback: () => 'Hello world!', methods: ['get'], }, }, }; hadron(expressApp, [require('@brainhubeu/hadron-express')], config).then(() => { expressApp.listen(port, () => console.log(`Listening on http://localhost:${port}`), ); }); ``` In the sections below, we will describe step by step what just happened. ## Bootstrapping an app The main hadron-core function is responsible for bootstrapping the app. It registers packages based on passed config and server instance: ```javascript const hadron = require('hadron-core').default; hadron(serverInstance, [...packages], config); ``` The purpose of the main function is to initialize DI container and register package dependencies according to correspondent sections in config object (described in details in next chapters). Main function returns a promise that resolves to created DI container instance. In the promise `.then()` method, besides performing operations on the container instance, we can actually start our server, by calling Express `listen` method: ```javascript hadron(serverInstance, ...rest).then((container) => { // do some things on container... serverInstance.listen(PORT, callback); }); ``` Now, let's move to DI container itself. ## Dependency Injection The whole framework is built around DI Container concept. Its purpose is to automatically supply proper arguments for routes callbacks and other framework's building blocks. DI container instance is created and used internally by bootstrapping function, it is also returned (as a promise) from bootstrapping function, as mentioned in the previous section. ### Container methods #### Registering items ```javascript container.register(key, item, lifetime); ``` * `key` - item name on which it will be registered inside the container * `item` - any value (primitive, data structure, function, class, etc.) * `lifetime` - the type of item's life-span Lifetime options: * `'value'` - container returns registered item as is [default] * `'singleton'` - returns always the same instance of registered class / constructor function * `'transient'` - returns always a new instance of registered class / constructor function #### Retrieving items ```javascript container.take(key); ``` * `key` - item name (same as provided during registration) The method returns item or item instance according to item type and lifetime option. #### Example usage in bootstrapping function ```javascript const { default: hadron, Lifetime } = require('hadron-core'); hadron(...args).then((container) => { container.register('foo', 123); container.register('bar', class Bar {}, Lifetime.Singleton); container.register('baz', class Baz {}, Lifetime.Transient); // other stuff... }); ``` ### Accessing container items from routes' callbacks To access container items from callbacks, you can just set arguments' names to match container keys, and required dependency will be provided. See an example [here](../routing/#retrieving-items-from-container-in-callback) ================================================ FILE: packages/hadron-core/index.ts ================================================ import hadronCore from './src/hadronCore'; export { default as Container } from './src/container/container'; export * from './src/container/lifecycle'; export * from './src/container/types'; export default hadronCore; ================================================ FILE: packages/hadron-core/package.json ================================================ { "name": "@brainhubeu/hadron-core", "version": "1.0.0", "description": "Hadron core module", "main": "dist/index.js", "files": [ "dist", "./LICENSE" ], "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "prepare": "tsc" }, "keywords": [ "hadron", "brainhub", "hadron-core" ], "author": "Brainhub", "license": "MIT", "dependencies": { "@brainhubeu/hadron-error-handler": "^1.0.0", "@brainhubeu/hadron-utils": "^1.0.0", "bunyan": "^1.8.12" }, "publishConfig": { "access": "public" } } ================================================ FILE: packages/hadron-core/src/__tests__/hadronCore.test.ts ================================================ import { expect } from 'chai'; import * as sinon from 'sinon'; import container from '../container/container'; import hadronCore, { prepareConfig } from '../hadronCore'; describe('prepareConfig()', () => { const logger = { error: sinon.spy() }; const defaultConfig = { prop1: 1, prop2: 2 }; it('should accept object', () => { const config = { prop2: 4, prop3: 5 }; const expectedResult = { prop1: 1, prop2: 4, prop3: 5 }; return prepareConfig(defaultConfig, config, logger).then((resolvedConfig) => expect(resolvedConfig).to.deep.equal(expectedResult), ); }); it('should accept promise, which returns object', () => { const config = new Promise((res, rej) => res({ prop2: 4, prop3: 5 })); const expectedResult = { prop1: 1, prop2: 4, prop3: 5 }; return prepareConfig(defaultConfig, config, logger).then((resolvedConfig) => expect(resolvedConfig).to.deep.equal(expectedResult), ); }); it('should log error when config promise is rejected', () => { const config = new Promise((res, rej) => rej()); const loggerMock = { error: sinon.spy() }; return prepareConfig(defaultConfig, config, loggerMock).catch((error) => expect(loggerMock.error.called).to.be.eql(true), ); }); it('should return default config when config promise is rejected', () => { const config = new Promise((res, rej) => rej()); return prepareConfig(defaultConfig, config, logger).then((resolvedConfig) => expect(resolvedConfig).to.deep.equal(defaultConfig), ); }); }); describe('hadronCore()', () => { const mockRegister = sinon.stub(container, 'register'); beforeEach(() => { return mockRegister.reset(); }); after(() => { return mockRegister.restore(); }); it('should return promise with Container instance', () => { return hadronCore({}).then((returnedContainer) => expect(returnedContainer).to.equal(container), ); }); it('should register server in container', () => { const server = { call: 'I am server!' }; return hadronCore(server).then((returnedContainer) => expect(mockRegister.calledWith('server', server)).to.be.eql(true), ); }); it('should run register function from given package', () => { const server = { call: 'I am server!' }; const mockPackage = { register: sinon.spy() }; return hadronCore(server, [mockPackage]).then((returnedContainer) => expect(mockPackage.register.calledOnce).to.be.eql(true), ); }); it('should include given config to hadron configuration and pass it to register function of package', () => { const testConfig = { testField: 'I am test!', }; const server = { call: 'I am server!' }; const mockPackage = { register: sinon.spy() }; return hadronCore(server, [mockPackage], testConfig).then( (returnedContainer) => // second argument, which should be config expect(mockPackage.register.args[0][1]).to.contain(testConfig), ); }); }); ================================================ FILE: packages/hadron-core/src/constants/eventNames.ts ================================================ export enum eventNames { HANDLE_INITIALIZE_APPLICATION_EVENT = 'handleInitializeApplicationEvent', HANDLE_TERMINATE_APPLICATION_EVENT = 'handleTerminateApplicationEvent', } ================================================ FILE: packages/hadron-core/src/container/__tests__/container.ts ================================================ /* tslint:disable:max-classes-per-file */ import { expect } from 'chai'; import container from '../container'; import { Lifecycle } from '../lifecycle'; describe('container register', () => { it('should overrive value for the the same key', () => { const itemName = 'test'; container.register(itemName, 'given'); container.register(itemName, 'given2'); expect('given2').to.equal(container.take(itemName)); }); it('should always return the same object - Singleton', () => { const itemName = 'test'; class Foo { public value: string; constructor() { this.value = new Date().getTime().toString(); } } container.register(itemName, Foo, Lifecycle.Singleton); const item1 = container.take(itemName); const item2 = container.take(itemName); expect(item2).to.deep.equal(item1); }); it('should always return a new same object - Transient', () => { class Foo { public value: string; constructor() { this.value = 'xxxx'; } } container.register('test', Foo, Lifecycle.Transient); const item1 = container.take('test'); const item2 = container.take('test'); expect(item1).to.deep.equal(item2); }); }); describe('container items with parameters in constructor', () => { it('second level of injection', () => { class Foo { public value: number; constructor() { this.value = 4; } } class Foo2 { public value: number; constructor(parameterName: Foo) { this.value = parameterName.value; } } container.register('parameterName', Foo, Lifecycle.Transient); container.register('foo2', Foo2, Lifecycle.Transient); const item = container.take('parameterName') as Foo; expect(4).to.be.equal(item.value); }); }); describe("list of container's items keys ", () => { it('should return array of keys of earlier registered items', () => { container.register('key1', 'item1'); container.register('key2', 'item2'); const keys = container.keys(); expect(keys).to.include('key1'); expect(keys).to.include('key2'); }); }); ================================================ FILE: packages/hadron-core/src/container/__tests__/containerItem.ts ================================================ /* tslint:disable:max-classes-per-file */ import { expect } from 'chai'; import containerItem from '../containerItem'; import { Lifecycle } from '../lifecycle'; describe('containerItem set lifecycle', () => { it('should be default(value)', () => { const item = containerItem.containerItemFactory('object', Object); expect(item.constructor.name).to.equal('ContainerItem'); }); it('should be transient', () => { const item = containerItem.containerItemFactory( 'object1', Object, 'transient', ); expect(item.constructor.name).to.equal('ContainerItemTransient'); }); it('should be singleton', () => { const item = containerItem.containerItemFactory( 'object2', Object, Lifecycle.Singleton, ); expect(item.constructor.name).to.equal('ContainerItemSingleton'); }); }); describe('containerItem set value', () => { it('should return 1', () => { const item = containerItem.containerItemFactory('number', 5); expect(item.Item).to.equal(5); }); it("should return 'oko'", () => { const item = containerItem.containerItemFactory('string', 'oko'); expect(item.Item).to.equal('oko'); }); it("should return '{}'", () => { const item = containerItem.containerItemFactory('object', {}); expect(item.Item).to.deep.equal({}); }); it('should return the same object twice', () => { class Foo { public value: string; constructor() { this.value = new Date().getTime().toString(); } } const item = containerItem.containerItemFactory( 'Fooooo', Foo, Lifecycle.Singleton, ); const item1 = item.Item; const item2 = item.Item; expect(item1).to.equal(item2); }); it('should always return new instance of given type', (done) => { class Foo { public value: string; constructor() { this.value = new Date().getTime().toString(); } } const item = containerItem.containerItemFactory( 'Foo', Foo, Lifecycle.Transient, ); const item1 = item.Item; setTimeout(() => { const item2 = item.Item; expect(item1.value).to.not.equal(item2.value); done(); }, 10); }); it('should always return new instance of given type 2', () => { class Foo { public value: string; constructor() { this.value = new Date().getTime().toString(); } } const item = containerItem.containerItemFactory( 'Foo', Foo, Lifecycle.Transient, ); const item1 = item.Item; const item2 = item.Item; expect(item1).to.not.equal(item2); }); }); ================================================ FILE: packages/hadron-core/src/container/container.ts ================================================ import containerItem from './containerItem'; import { IContainerItem, IContainer } from './types'; import isVarName from '../helpers/isVarName'; import IncorrectContainerKeyNameError from '../errors/IncorrectContainerKeyNameError'; const containerRegister = new Array(); const takeContainerByKey = (key: string): IContainerItem[] => containerRegister.filter((x) => x.getKey() === key); /* method for registering items in container */ /** * Method for registering items in container, optionally setting a lifespan for items * @param key for representing item in register, second use of the same key override previous item * @param item stored item, can be any type: value, type, function.... * @param lifecycle setting type of life-span [value, singleton, transient] - value is default */ const register = (key: string, item: any, lifecycle?: string): void => { if (!isVarName(key)) { throw new IncorrectContainerKeyNameError(key); } const containerItems = takeContainerByKey(key); if (containerItems.length === 0) { const ci = containerItem.containerItemFactory(key, item, lifecycle); containerRegister.push(ci); } else { containerItems[0].Item = item; } }; /** method for getting item from register * Method which returns item previously registered for passed key * @param key for representing item in register * @return { any } return stored item */ const take = (key: string): any => { const containerItems = takeContainerByKey(key); return containerItems.length === 0 ? null : containerItems[0].Item; }; /** method for getting all the keys in container * @return array of keys */ const keys = (): string[] => containerRegister.map((x) => x.getKey()); const container = { register, take, keys, }; export default container as IContainer; ================================================ FILE: packages/hadron-core/src/container/containerItem.ts ================================================ import container from './container'; import { IContainerItem } from './types'; import { Lifecycle } from './lifecycle'; import { getArgs } from '@brainhubeu/hadron-utils'; export class ContainerItem implements IContainerItem { // tslint:disable-next-line:variable-name constructor(protected key: string, protected item: any) {} get Item(): any { return this.item; } set Item(item: any) { this.item = item; } public getKey() { return this.key; } public getArgs(): string[] { return getArgs(this.item); } } // tslint:disable-next-line:max-classes-per-file class ContainerItemSingleton extends ContainerItem { // tslint:disable-next-line:variable-name private _itemInstanse: any; constructor(key: string, item: any) { super(key, item); this._itemInstanse = null; } set Item(item: any) { this.item = item; } get Item(): any { const parameters = this.getArgs(); if (parameters.length > 0) { const parameterInstances = parameters.map((paramName) => container.take(paramName), ); if (this._itemInstanse === null) { try { this._itemInstanse = new this.item(...parameterInstances); } catch (error) { throw new Error(`can not create an instance of ${this.key}`); } } return this._itemInstanse; } if (this._itemInstanse === null) { try { this._itemInstanse = new this.item(); } catch (error) { throw new Error(`can not create an instance of ${this.key}`); } } return this._itemInstanse; } } // tslint:disable-next-line:max-classes-per-file class ContainerItemTransient extends ContainerItem { constructor(key: string, item: any) { super(key, item); } set Item(item: any) { this.item = item; } get Item(): any { const parameters = this.getArgs(); if (parameters.length > 0) { const parameterInstances = parameters.map((paramName) => container.take(paramName), ); try { return new this.item(...parameterInstances); } catch (error) { throw new Error(`can not create a new instance of ${this.key}`); } } else { try { return new this.item(); } catch (error) { throw new Error(`can not create a new instance of ${this.key}`); } } } } const containerItemFactory = ( key: string, item: any, lifecycle?: string, ): ContainerItem => { switch (lifecycle) { case Lifecycle.Singleton: return new ContainerItemSingleton(key, item); case Lifecycle.Transient: return new ContainerItemTransient(key, item); default: return new ContainerItem(key, item); } }; export default { containerItemFactory }; ================================================ FILE: packages/hadron-core/src/container/lifecycle.ts ================================================ enum Lifecycle { Transient = 'transient', Singleton = 'singleton', Value = 'value', } export { Lifecycle }; ================================================ FILE: packages/hadron-core/src/container/types.ts ================================================ interface IContainerItem { Item(): any; Item(key: string): void; getKey(): string; getArgs(): string[]; } interface IContainer { take: (key: string) => any; register: (key: string, value: any, lifecycle?: string) => any; keys: () => string[]; } export { IContainerItem, IContainer }; ================================================ FILE: packages/hadron-core/src/declarations.d.ts ================================================ declare module '@hadron/utils'; ================================================ FILE: packages/hadron-core/src/errors/IncorrectContainerKeyNameError.ts ================================================ import HadronErrorHandler from '@brainhubeu/hadron-error-handler'; export default class IncorrectContainerKeyNameError extends HadronErrorHandler { constructor(key: string, err: Error = new Error()) { super(); this.message = `The key name '${key}' is incorrect to register in container, should be a valid variable name.`; this.name = 'IncorrectContainerKeyNameError'; this.stack = null; this.error = err; } } ================================================ FILE: packages/hadron-core/src/errors/LoadingPackageError.ts ================================================ import HadronErrorHandler from '@brainhubeu/hadron-error-handler'; export default class LoadingPackageError extends HadronErrorHandler { constructor(err: Error) { super(); this.message = `Problem with loading package`; this.name = 'LoadingPackageError'; this.stack = null; this.error = err; } } ================================================ FILE: packages/hadron-core/src/hadronCore.ts ================================================ import container from './container/container'; import { IContainer } from './container/types'; import { createLogger } from 'bunyan'; const hadronDefaultConfig = {}; export const prepareConfig = ( defaultConfig: object | Promise, config: object | Promise, logger: any, ) => { return Promise.all([defaultConfig, config]) .then(([resolvedDefaultConfig, resolvedConfig]) => ({ ...resolvedDefaultConfig, ...resolvedConfig, })) .catch((err) => { logger.error(`Config promise rejected: ${err}`); return defaultConfig; }); }; export default ( server: any, packages: any[] = [], config: any = {}, ): Promise => { container.register('server', server); const logger = createLogger({ name: 'hadron-logger' }); container.register('hadronLogger', logger); return prepareConfig(hadronDefaultConfig, config, logger).then( (hadronConfig) => { return Promise.all( packages .filter(({ register }) => !!register) .map(({ register }) => register(container, hadronConfig)), ).then(() => container); }, ); }; ================================================ FILE: packages/hadron-core/src/helpers/__tests__/isVarName.ts ================================================ import { assert } from 'chai'; import isVarName from '../isVarName'; describe('isVarName', () => { it('when provided "variable" should return true', () => { assert(isVarName('variable')); }); it('when provided "someLongVariableNameMayBeCorrect" should return true', () => { assert(isVarName('someLongVariableNameMayBeCorrect')); }); it('when provided "delete" should return true', () => { assert(isVarName('delete')); }); it('when provided "var" should return true', () => { assert(isVarName('var')); }); it('when provided "do" should return true', () => { assert(isVarName('do')); }); it('when provided "1" should return false', () => { assert(!isVarName('1')); }); it('when provided "1variable" should return false', () => { assert(!isVarName('1variable')); }); it('when provided "-v" should return false', () => { assert(!isVarName('-v')); }); it('when provided "variable-name" should return false', () => { assert(!isVarName('variable-name')); }); it('when provided "variable name" should return false', () => { assert(!isVarName('variable name')); }); }); ================================================ FILE: packages/hadron-core/src/helpers/isVarName.ts ================================================ // tslint:disable:no-eval export default function isVarName(str: string): boolean { if (typeof str !== 'string') { return false; } if (str.trim() !== str) { return false; } if (!isNaN(parseInt(str[0], null))) { return false; } try { eval(`var temp = { ${str}: null }`); } catch (e) { return false; } return true; } ================================================ FILE: packages/hadron-core/tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "target": "es6", "noImplicitAny": true, "noEmitOnError": true, "moduleResolution": "node", "sourceMap": true, "outDir": "dist", "baseUrl": ".", "paths": { "*": ["./*"] }, "emitDecoratorMetadata": true, "experimentalDecorators": true, "inlineSources": true, "lib": ["es2017", "dom"] }, "include": ["src/**/*.*", "index.ts", "LICENSE"], "exclude": ["node_modules", "**/__tests__/**", "**/*/*.d.ts"] } ================================================ FILE: packages/hadron-demo/LICENSE ================================================ MIT License Copyright (c) 2018 Brainhub Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/hadron-demo/README.md ================================================ # Demo App for Hadron framework To start just run ```bash npm run start ``` Requires TS-Node (for now) ================================================ FILE: packages/hadron-demo/declarations.d.ts ================================================ declare module 'xmljson'; declare module 'glob'; declare module 'xml2js'; declare module '*.json' { const value: any; // @ts-ignore export default value; } declare module '*.js' { const value: any; // @ts-ignore export default value; } ================================================ FILE: packages/hadron-demo/entity/Role.ts ================================================ import { IRole } from '@brainhubeu/hadron-auth'; import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; @Entity() export class Role implements IRole { @PrimaryGeneratedColumn() public id: string | number; @Column({ type: 'text' }) public name: string; } ================================================ FILE: packages/hadron-demo/entity/Team.ts ================================================ import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; import { User } from './User'; @Entity() export class Team { @PrimaryGeneratedColumn() public id: number; @Column({ type: 'text' }) public name: string; @OneToMany((type) => User, (user) => user.team) public users: User[]; } ================================================ FILE: packages/hadron-demo/entity/User.ts ================================================ import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, ManyToMany, JoinTable, } from 'typeorm'; import { Team } from './Team'; import { IUser, IRole } from '@brainhubeu/hadron-auth'; import { Role } from './Role'; @Entity() export class User implements IUser { @PrimaryGeneratedColumn() public id: number; @Column({ type: 'text' }) public username: string; @Column({ type: 'text' }) public passwordHash: string; @ManyToOne((type) => Team, (team) => team.users) public team: Team; @ManyToMany((type) => Role, { cascadeInsert: true, cascadeUpdate: true, }) @JoinTable({ name: 'user_role' }) public roles: IRole[]; } ================================================ FILE: packages/hadron-demo/entity/validation/schemas.ts ================================================ import insertTeam = require('./team/insertTeam.json'); import updateTeam = require('./team/updateTeam.json'); import insertUser = require('./user/insertUser.json'); import updateUser = require('./user/updateUser.json'); export default { insertTeam, updateTeam, insertUser, updateUser, }; ================================================ FILE: packages/hadron-demo/entity/validation/team/insertTeam.json ================================================ { "type": "object", "properties": { "teamName": { "type": "string" } }, "required": ["teamName"], "additionalProperties": false } ================================================ FILE: packages/hadron-demo/entity/validation/team/updateTeam.json ================================================ { "type": "object", "properties": { "id": { "type": "number" }, "teamName": { "type": "string" } }, "required": ["id", "teamName"], "additionalProperties": false } ================================================ FILE: packages/hadron-demo/entity/validation/user/insertUser.json ================================================ { "type": "object", "properties": { "username": { "type": "string" }, "password": { "type": "string" }, "teamId": { "type": "number" } }, "required": ["username", "password", "teamId"], "additionalProperties": false } ================================================ FILE: packages/hadron-demo/entity/validation/user/updateUser.json ================================================ { "type": "object", "properties": { "id": { "type": "number" }, "username": { "type": "string" }, "teamId": { "type": "number" } }, "required": ["id", "username", "teamId"], "additionalProperties": false } ================================================ FILE: packages/hadron-demo/entity/validation/validate.ts ================================================ import validatorFactory from '../../../hadron-validation'; import schemas from './schemas'; export default validatorFactory(schemas); ================================================ FILE: packages/hadron-demo/event-emitter/case1/index.ts ================================================ import eventManagerProvider, { IEventsConfig } from '@brainhubeu/hadron-events'; import { EventEmitter } from 'events'; const emitter = new EventEmitter(); const config = {} as IEventsConfig; const eventManager = eventManagerProvider(emitter, config); const listeners = [ { name: 'LISTENER', event: 'testEvent', // event to listen to handler: (callback: any, ...args: any[]) => { const result = callback(...args); return `${result}-changed`; }, }, ]; eventManager.registerEvents(listeners); const callback = () => 'testcase'; const newCallback = eventManager.emitEvent('testEvent', callback); newCallback(); ================================================ FILE: packages/hadron-demo/event-emitter/case2/index.ts ================================================ import eventManagerProvider, { IEventsConfig } from '@brainhubeu/hadron-events'; import { EventEmitter } from 'events'; const emitter = new EventEmitter(); const config = {} as IEventsConfig; const eventManager = eventManagerProvider(emitter, config); const listeners = [ { name: 'LISTENER', event: 'testEvent', // event to listen to handler: () => 'test console log', }, ]; eventManager.registerEvents(listeners); const callback = () => 'testcase'; const newCallback = eventManager.emitEvent('testEvent', callback); newCallback(); ================================================ FILE: packages/hadron-demo/event-emitter/case3/index.ts ================================================ import eventManagerProvider, { IEventsConfig } from '@brainhubeu/hadron-events'; import { EventEmitter } from 'events'; const emitter = new EventEmitter(); const config = {} as IEventsConfig; const eventManager = eventManagerProvider(emitter, config); const listeners = [ { name: 'LISTENER', event: 'testEvent', // event to listen to handler: (callback: any, ...args: any[]) => { const time1 = Date.now(); callback(...args); const time2 = Date.now(); return time2 - time1; }, }, ]; eventManager.registerEvents(listeners); const callback = () => 'testcase'; const newCallback = eventManager.emitEvent('testEvent', callback); newCallback(); ================================================ FILE: packages/hadron-demo/event-emitter/config.ts ================================================ import { IEventsConfig } from '@brainhubeu/hadron-events'; const emitterConfig: IEventsConfig = { listeners: [ { name: 'LISTENER-1', event: 'handleTerminateApplicationEvent', // event to listen to handler: (callback: any, ...args: any[]) => { const cb = () => { callback(...args); }; return cb(); }, }, { name: 'LISTENER-2', event: 'handleInitializeApplicationEvent', // event to listen to handler: () => { // console.log('-----------app started-----------') }, }, ], }; export default emitterConfig; ================================================ FILE: packages/hadron-demo/express-demo/index.ts ================================================ import { Container as container } from '@brainhubeu/hadron-core'; const getDate = () => { const d = new Date(); return `${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}`; }; export default { routes: { // // Basic Hello World // helloWorldRoute: { path: '/', callback: () => 'Hello world', methods: ['get'], }, // // Route containing single middleware // singleMiddleware: { path: '/singleMiddleware', callback: () => `Hey! See console`, methods: ['get'], middleware: [ (req: any, res: any, next: any) => { // tslint:disable:no-console console.log(`Hello, it's me, the very first middleware!`); next(); }, ], }, // // Using multiple middlewares // multipleMiddlewares: { path: '/multipleMiddlewares', callback: () => `Hey! See console`, methods: ['get'], middleware: [ (req: any, res: any, next: any) => { // tslint:disable:no-console console.log( `${getDate()}> First middleware, jump to next middleware in one second.`, ); setTimeout(() => { next(); }, 1000); }, (req: any, res: any, next: any) => { // tslint:disable:no-console console.log( `${getDate()}> Finally second middleware. One second passed i think !`, ); next(); }, ], }, // // Load value registered in container // containerKey: { path: '/getContainerValue', methods: ['get'], callback: (req: any, { customValue }: any) => customValue, }, // // Load custom value registered in container // customContainerKey: { path: '/getContainerValue/:key', methods: ['get'], callback: (req: any, { key }: { key: string }) => container.take(key), }, // // Display URL parameter // routeWithParam: { path: '/:param', methods: ['get'], callback: ({ params }: any) => `First route param: ${params.param}`, }, // // Display multiple URL parameters // routeWithMultipleParams: { path: '/:param/:param2', methods: ['get'], callback: ({ params }: any) => `First param value: ${params.param}; Second param value: ${ params.param2 }`, }, // // Register container value under specific key // registerCustomValue: { path: '/:key', methods: ['post'], callback: ({ params, body }: any) => { container.register(params.key, body.value); return `Value under key '${params.key}' is registered`; }, }, // // Clear value under specific key in container // deleteCustomValue: { path: '/:key', methods: ['delete'], callback: ({ params }: any) => { container.register(params.key, null); return `Value under key '${params.key}' has been deleted`; }, }, }, }; ================================================ FILE: packages/hadron-demo/index.ts ================================================ import * as bodyParser from 'body-parser'; import * as express from 'express'; import hadron, { IContainer } from '@brainhubeu/hadron-core'; import * as hadronEvents from '@brainhubeu/hadron-events'; import * as hadronSerialization from '@brainhubeu/hadron-serialization'; import * as hadronExpress from '@brainhubeu/hadron-express'; import * as hadronLogger from '@brainhubeu/hadron-logger'; import * as hadronTypeOrm from '@brainhubeu/hadron-typeorm'; import * as hadronAuth from '@brainhubeu/hadron-auth'; import jsonProvider from '@brainhubeu/hadron-json-provider'; import expressConfig from './express-demo'; import typeormConfig from './typeorm-demo/index'; import emitterConfig from './event-emitter/config'; import serializationRoutes from './serialization/routing'; import { setupSerializer } from './serialization/serialization-demo'; import 'reflect-metadata'; import securedRoutes from './security/securedRoutesConfig'; const port = process.env.PORT || 8080; const expressApp = express(); expressApp.use(bodyParser.json()); jsonProvider([`${__dirname}/routing/*`], ['config.js']).then((routes: any) => { const config = { securedRoutes, ...typeormConfig, ...hadronLogger, events: emitterConfig, routes: { ...serializationRoutes, ...routes, ...expressConfig.routes, }, }; hadron( expressApp, [ hadronAuth, hadronEvents, hadronSerialization, hadronTypeOrm, hadronExpress, ], config, ) .then((container: IContainer) => { expressApp.use((req, res, next) => res.status(404).json('Endpoint not found.'), ); container.register('customValue', 'From Brainhub with ❤️'); setupSerializer(); expressApp.listen(port); }) .catch(console.log); return; }); ================================================ FILE: packages/hadron-demo/logger/adapters/winstonAdapter.ts ================================================ import * as winston from 'winston'; import { ILogger } from '@brainhubeu/hadron-logger'; export default (config: any): ILogger => { winston.loggers.add(config.name, config); const logger = winston.loggers.get(config.name); return { log: (message: string) => { logger.info(message); }, debug: (message: string) => { logger.debug(message); }, warn: (message: string) => { logger.warn(message); }, error: (message: string) => { logger.error(message); }, }; }; ================================================ FILE: packages/hadron-demo/logger/index.ts ================================================ export default { logger: [ { type: 'bunyan', name: 'first logger', }, { type: 'winston', name: 'second', }, { name: 'third ', }, ], }; ================================================ FILE: packages/hadron-demo/package.json ================================================ { "name": "@brainhubeu/hadron-demo", "version": "1.1.4", "description": "Hadron demo example app", "main": "dist/index.js", "scripts": { "start": "ts-node index.ts", "test": "echo \"Error: no test specified\" && exit 1", "prepare": "tsc" }, "keywords": [ "hadron", "brainhub", "hadron-demo" ], "dependencies": { "@brainhubeu/hadron-auth": "^0.0.2", "@brainhubeu/hadron-core": "^1.0.0", "@brainhubeu/hadron-events": "^1.0.1", "@brainhubeu/hadron-express": "^2.0.0", "@brainhubeu/hadron-json-provider": "^1.0.0", "@brainhubeu/hadron-logger": "^1.0.1", "@brainhubeu/hadron-serialization": "^1.0.0", "@brainhubeu/hadron-typeorm": "^1.0.3", "@brainhubeu/hadron-utils": "^1.0.0", "@brainhubeu/hadron-validation": "^1.0.0", "bcrypt": "^2.0.1", "body-parser": "1.18.2", "cors": "2.8.4", "dotenv": "4.0.0", "express": "4.16.2", "jsonwebtoken": "^8.2.2", "multer": "1.3.0", "mysql": "2.15.0", "reflect-metadata": "^0.1.12", "typeorm": "0.1.18" }, "devDependencies": { "@types/body-parser": "1.16.8", "@types/cors": "2.8.3", "@types/dotenv": "4.0.2", "@types/express": "4.11.1", "@types/multer": "1.3.6", "@types/node": "9.6.0" }, "author": "Brainhub", "license": "MIT", "publishConfig": { "access": "public" } } ================================================ FILE: packages/hadron-demo/performance-tests/README.md ================================================ # Performance Tests ## Testing toolkit Artillery - [Artillery website](https://artillery.io/) ## Basic commmands * duration: x - phase will last for x seconds * arrivalRate: x - x new virtual users will arrive every second in phase ## Example insertUser.yml ``` config: target: 'http://localhost:8080' phases: - duration: 10 arrivalRate: 5 defaults: scenarios: - flow: - loop: - post: url: '/insertUser' json: userName: 'Test' teamId: 1 count: 3 ``` ## How to run * First we need to install Artillery with npm ``` npm install -g artillery ``` * Now we can do a quick test: ``` artillery quick --count 10 -n 20 http://localhost:8080/user ``` Where: count - amount of virtual users, n - each virtual user will send 20 GET requests. Of course to make it work on localhost:8080/\* **_we need to run hadron-demo with sql_** * Artillery also provides test scripts as in example section. We can run test script with command: ``` artillery run filename.yml ``` ## Output ``` All virtual users finished Summary report @ 15:10:41(+0200) 2018-04-05 Scenarios launched: 609 Scenarios completed: 609 Requests completed: 1827 RPS sent: 29.64 Request latency: min: 4.2 max: 4905.6 median: 184.7 p95: 2472.6 p99: 4358.4 Scenario counts: Inserting, updating, deleting and searching teams: 609 (100%) Codes: 200: 609 201: 1218 ``` Where: * **_Scenarios launched_** - number of virtual users created * **_Scenarios completed_** - number of virtual users completed their scenarios * **_Request completed_** - number of HTTP requests or responses sent * **_RPS sent_** - average number of requests per second * **_Request latency_** - are in milliseconds. (a request latency p99 value of 500ms means that 99 out of 100 requests took 500ms or less to complete) ================================================ FILE: packages/hadron-demo/performance-tests/teamService/getTeams.yml ================================================ config: target: 'http://localhost:8080' phases: - duration: 10 arrivalRate: 5 defaults: scenarios: - flow: - get: url: '/team' ================================================ FILE: packages/hadron-demo/performance-tests/teamService/insertTeam.yml ================================================ config: target: 'http://localhost:8080' phases: - duration: 10 arrivalRate: 5 defaults: scenarios: - flow: - loop: - post: url: '/insertTeam' json: teamName: 'Test Team' count: 3 ================================================ FILE: packages/hadron-demo/performance-tests/teamService/team.yml ================================================ config: target: 'http://localhost:8080' phases: - duration: 10 arrivalRate: 5 - duration: 20 arrivalRate: 10 rampTo: 30 - duration: 30 arrivalRate: 5 scenarios: - name: 'Inserting, updating, deleting and searching teams' flow: - post: url: '/insertTeam' json: teamName: 'Team Test' - put: url: '/updateTeam' json: id: 2 teamName: 'Team 2' - get: url: '/team' ================================================ FILE: packages/hadron-demo/performance-tests/teamService/updateTeam.yml ================================================ config: target: 'http://localhost:8080' phases: - duration: 10 arrivalRate: 5 defaults: scenarios: - flow: - loop: - put: url: '/updateTeam' json: id: 1 teamName: 'Updated team!' count: 3 ================================================ FILE: packages/hadron-demo/performance-tests/userService/getUsers.yml ================================================ config: target: 'http://localhost:8080' phases: - duration: 10 arrivalRate: 5 defaults: scenarios: - flow: - get: url: '/user' ================================================ FILE: packages/hadron-demo/performance-tests/userService/insertUser.yml ================================================ config: target: 'http://localhost:8080' phases: - duration: 10 arrivalRate: 5 defaults: scenarios: - flow: - loop: - post: url: '/insertUser' json: userName: 'Test' teamId: 1 count: 3 ================================================ FILE: packages/hadron-demo/performance-tests/userService/updateUser.yml ================================================ config: target: 'http://localhost:8080' phases: - duration: 10 arrivalRate: 5 defaults: scenarios: - flow: - loop: - put: url: '/updateUser' json: id: 2 userName: 'Test' teamId: 1 count: 3 ================================================ FILE: packages/hadron-demo/performance-tests/userService/user.yml ================================================ config: target: 'http://localhost:8080' phases: - duration: 10 arrivalRate: 5 - duration: 20 arrivalRate: 10 rampTo: 30 - duration: 30 arrivalRate: 5 scenarios: - name: 'Inserting, updating, deleting and searching users' flow: - post: url: '/insertUser' json: userName: 'Test' teamId: 1 - put: url: '/updateUser' json: id: 2 userName: 'BB' teamId: 1 - delete: url: '/deleteUser/3' - get: url: '/user' ================================================ FILE: packages/hadron-demo/routing/home.config.js ================================================ const helloWorldCallback = () => 'Hello world'; const versionCallback = () => `Version: ${process.env.VERSION || '0.0.1'}`; const login = require('../security/loginRoute').default; const homeConfig = () => { return { helloWorldRoute: { callback: helloWorldCallback, methods: ['GET'], path: '/', }, versionRoute: { callback: versionCallback, methods: ['get'], path: '/version', }, login: { callback: login, methods: ['POST'], path: '/login', }, }; }; module.exports = homeConfig; ================================================ FILE: packages/hadron-demo/routing/nested-routes.config.js ================================================ const testMiddleware = (req, res, next) => { res.locals.injected = 'I was injected here!'; next(); }; const teamRoutsConfig = () => { return { nestedRoutes: { callback: (req, container, locals) => ({ body: { response: 'Hello There!', locals }, }), path: '/test/', methods: ['GET'], middleware: [testMiddleware], routes: { route1: { callback: (req, container, locals) => ({ body: { response: 'General Kenobi', locals }, }), methods: ['GET'], path: '/route1/', }, deepRoute1: { callback: (req, container, locals) => ({ body: { response: 'You are a bold one!', locals }, }), methods: ['POST'], $middleware: [], path: '/route2/', }, route3: { callback: (req, container, locals) => ({ body: { response: 'Kill him...', locals }, }), methods: ['GET'], middleware: [ (req, res, next) => { res.locals.additionalInjection = 'Some tasty addition'; next(); }, ], $path: '/route3/', routes: { deepRoute1: { path: '/deepRoute/', callback: (req, container, locals) => ({ body: { response: 'Kill him...', locals }, }), }, }, }, }, }, }; }; module.exports = teamRoutsConfig; ================================================ FILE: packages/hadron-demo/routing/team.config.js ================================================ const teamService = require('../services/teamService'); const teamRoutsConfig = () => { return { getTeams: { callback: teamService.getAllTeams, methods: ['GET'], path: '/team/', }, getTeamById: { callback: teamService.getTeamById, methods: ['GET'], path: '/team/:id', }, insertTeam: { callback: teamService.insertTeam, methods: ['POST'], path: '/team', }, updateTeam: { callback: teamService.updateTeam, methods: ['PUT'], path: '/team', }, deleteTeam: { callback: teamService.deleteTeam, methods: ['DELETE'], path: '/team/:id', }, }; }; module.exports = teamRoutsConfig; ================================================ FILE: packages/hadron-demo/routing/user.config.js ================================================ const userService = require('../services/userService'); const userRoutsConfig = () => { return { getAllUsers: { callback: userService.getAllUsers, methods: ['GET'], path: '/user/', }, getUserById: { callback: userService.getUserById, methods: ['GET'], path: '/user/:id', }, insertUser: { callback: userService.insertUser, methods: ['POST'], path: '/user/', }, updateUser: { callback: userService.updateUser, methods: ['PUT'], path: '/user/', }, deleteUser: { callback: userService.deleteUser, methods: ['DELETE'], path: '/user/:id', }, }; }; module.exports = userRoutsConfig; ================================================ FILE: packages/hadron-demo/security/loginRoute.ts ================================================ import * as jwt from 'jsonwebtoken'; import { bcrypt } from '@brainhubeu/hadron-auth'; const secret = process.env.JWT_SECRET || 'H4DR0N_S3CUR17Y'; const unauthorized = { status: 403, body: { error: { message: 'Unauthorized', }, }, }; const login = async (req: any, { userRepository }) => { try { const user = await userRepository.findOne({ where: { username: req.body.username }, }); if (!user) { return unauthorized; } const validPassword = await bcrypt.compare( req.body.password, user.passwordHash, ); if (validPassword) { const token = jwt.sign( { id: user.id, username: user.username, }, secret, { expiresIn: '2h', }, ); return { status: 200, body: { token, }, }; } return unauthorized; } catch (error) { return unauthorized; } }; export default login; ================================================ FILE: packages/hadron-demo/security/securedRoutesConfig.ts ================================================ const securedRoutesConfig = [ { path: '/team/*', roles: [['Admin', 'User'], 'Manager'], }, { path: '/user/*', methods: ['GET'], roles: ['NotExists', 'User', 'Admin'], }, { path: '/user/*', methods: ['POST', 'PUT', 'DELETE'], roles: 'Admin', }, ]; export default securedRoutesConfig; ================================================ FILE: packages/hadron-demo/serialization/routing/index.ts ================================================ import unicornsRoutes from './unicorns'; import princessesRoutes from './princesses'; export default { ...unicornsRoutes, ...princessesRoutes, }; ================================================ FILE: packages/hadron-demo/serialization/routing/princesses.ts ================================================ import { Container } from '@brainhubeu/hadron-core'; import { princesses } from '../unicorns-and-princesses'; import { ISerializer } from '@brainhubeu/hadron-serialization'; Container.register('princesses', princesses); export default { getPrincess: { path: '/princesses/:name', callback: ({ params }: any, { princesses }: any) => { return { body: princesses[params.name], }; }, methods: ['get'], }, getPrincessWithRole: { path: '/princesses/:role/:name', callback: ({ params }: any, { princesses, serializer }: any) => { return { body: serializer.serialize( princesses[params.name], [params.role], 'Princess', ), }; }, methods: ['get'], }, getPrincessWithRoleAndSerializer: { path: '/princesses/:role/:name', callback: ({ params }: any, { princesses, princessSerializer }: any) => { return { body: princessSerializer(princesses[params.name], [params.role]), }; }, methods: ['get'], }, }; ================================================ FILE: packages/hadron-demo/serialization/routing/unicorns.ts ================================================ import { Container } from '@brainhubeu/hadron-core'; import { unicorns } from '../unicorns-and-princesses'; import { ISerializer } from '@brainhubeu/hadron-serialization'; Container.register('unicorns', unicorns); export default { getUnicorn: { path: '/unicorns/:name', callback: ({ params }: any, { unicorns }: any) => { return { body: unicorns[params.name] }; }, methods: ['get'], }, getUnicornWithRole: { path: '/unicorns/:role/:name', callback: ({ params }: any, { serializer, unicorns }: any) => { return { body: serializer.serialize( unicorns[params.name], [params.role], 'Unicorn', ), }; }, methods: ['get'], }, getUnicornWithRoleAndSerializer: { path: '/unicorns/:role/:name', callback: ({ params }: any, { unicorns, unicornSerializer }: any) => { return { body: unicornSerializer(unicorns[params.name], [params.role]), }; }, methods: ['get'], }, }; ================================================ FILE: packages/hadron-demo/serialization/schemas/princess.json ================================================ { "name": "Princess", "properties": [ { "name": "name", "type": "string" }, { "name": "address", "type": "string", "groups": ["admin"] }, { "name": "money", "type": "number", "parsers": ["currency"], "groups": ["admin"]}, { "name": "friends", "type": "array", "properties": [ { "name": "name", "type": "string" }, { "name": "profession", "type": "string", "groups": ["admin"] }, { "name": "salary", "type": "number", "parsers": ["currency"] } ] } ] } ================================================ FILE: packages/hadron-demo/serialization/schemas/unicorn.json ================================================ { "name": "Unicorn", "properties": [ { "name": "name", "type": "string" }, { "name": "hornLength", "type": "number", "groups": ["expert", "admin", "buyer"] }, { "name": "magicPower", "type": "object", "groups": ["expert", "admin", "buyer"], "properties" : [ { "name": "name", "type": "string" }, { "name": "power", "type": "number", "serializationName": "powerLevel" }, { "name": "magicSchool", "type": "string", "groups": ["expert"] } ] }, { "name": "price", "type": "number", "parsers": ["currency"], "groups": ["buyer", "admin"]} ] } ================================================ FILE: packages/hadron-demo/serialization/serialization-demo.ts ================================================ import { Container } from '@brainhubeu/hadron-core'; import { schemaProvider, CONTAINER_NAME, ISerializer, } from '@brainhubeu/hadron-serialization'; import { resolve } from 'path'; const paths = [resolve(__dirname, 'schemas/*')]; export const setupSerializer = () => schemaProvider(paths).then((schemas: any) => { const serializer: ISerializer = Container.take(CONTAINER_NAME); schemas.forEach(serializer.addSchema); serializer.addParser((value: any) => `${value}$`, 'currency'); }); export const serializeUnicorn = (unicornData: any, groups: string[] = []) => { const serializer = Container.take(CONTAINER_NAME) as ISerializer; return serializer.serialize(unicornData, groups, 'Unicorn'); }; Container.register('unicornSerializer', serializeUnicorn); export const serializePrincess = (unicornData: any, groups: string[] = []) => { const serializer = Container.take(CONTAINER_NAME) as ISerializer; return serializer.serialize(unicornData, groups, 'Princess'); }; Container.register('princessSerializer', serializePrincess); ================================================ FILE: packages/hadron-demo/serialization/unicorns-and-princesses.ts ================================================ export const unicorns = { arthur: { hornLength: '20', id: '10002', magicPower: { magicSchool: 'Fake', name: 'Power of Truth', power: '12', usability: '0', }, name: 'RainbowHoof', price: '2100', secretName: 'RainbowFart', }, cssdash: { hornLength: '13', id: 'd4sh', magicPower: { magicSchool: 'CSS', name: 'Power of Vertically Align Things', power: '8', usability: '100', }, name: 'Css Dash', price: '10000', psychologyProfile: 'Suicide Thoughts', }, }; export class Princess { public address: any = null; public friends: any = null; public id: any = null; public money: any = null; public name: any = null; constructor({ address, friends, id, money, name }: any) { this.address = address; this.friends = friends; this.id = id; this.money = money; this.name = name; } } export const princesses = { jasmine: new Princess({ address: 'Górnych Wałów 26/5', friends: [ { name: 'Francesca', salary: '5120', profession: 'Cooker', id: '123' }, { name: 'Marina', salary: '2010', profession: 'Gardener' }, { name: 'Robin', salary: '0', profession: 'Crime Fighter' }, ], id: '10002', money: '21000', name: 'Jasmine', }), veronica: new Princess({ address: 'Red Lanterns 66/6', friends: [ { name: 'Hilary', salary: '6000', profession: 'Not President', id: '123', }, { name: 'Andrzej L', salary: '3678.23', profession: 'Farmer', lpr: false, }, ], id: 'RT123', money: '12000', name: 'Veronic', }), jozin: new Princess({ address: 'Bazin', friends: [], id: '222', money: 0, name: 'Jozin', }), }; ================================================ FILE: packages/hadron-demo/services/teamService.ts ================================================ import { Team } from '../entity/Team'; import { User } from '../entity/User'; import { Repository } from 'typeorm'; import validate from '../entity/validation/validate'; class TeamDto { constructor(public id: number, public name: string, public amount: number) {} } const getAllTeams = async (req, { teamRepository }) => { const teams = await teamRepository.find({ relations: ['users'] }); return { body: teams.map( (team) => new TeamDto(team.id, team.name, team.users.length), ), }; }; const getTeamById = async ({ params }, { teamRepository }) => { return { body: await teamRepository.findOneById(params.id), }; }; const updateTeam = async ({ body }, { teamRepository }) => { try { await validate('updateTeam', body); const team = teamRepository.findOneById(body.id); team.name = body.teamName; await teamRepository.save(team); return { body: { message: `team id: ${body.id} has new name ${body.teamName}` }, }; } catch (error) { return { status: 400, body: { error: error.message }, }; } }; const insertTeam = async ({ body }, { teamRepository }) => { try { await validate('insertTeam', body); await teamRepository.insert({ name: body.teamName }); const amount = await teamRepository.count(); return { status: 201, body: { message: `total amount of teams: ${amount}` }, }; } catch (error) { return { status: 400, body: { error: error.message }, }; } }; const deleteTeam = async ( { params }: any, { teamRepository, userRepository }: any, ) => { const team = await teamRepository.findOneById(params.id, { relations: ['users'], }); await userRepository.removeByIds(team.users.map((user) => user.id)); return { body: await teamRepository.removeById(params.id), }; }; export { getAllTeams, getTeamById, updateTeam, insertTeam, deleteTeam }; ================================================ FILE: packages/hadron-demo/services/userService.ts ================================================ import { User } from '../entity/User'; import validate from '../entity/validation/validate'; import { Container } from '@brainhubeu/hadron-core'; import { bcrypt } from '@brainhubeu/hadron-auth'; import { Role } from '../entity/Role'; class UserDto { constructor( public id: number, public username: string, public teamName: string, public roles: Role[], ) {} } const getAllUsers = async (req, { userRepository }) => { const users = await userRepository.find({ relations: ['team', 'roles'] }); return { body: users.map( (user: User) => new UserDto(user.id, user.username, user.team.name, user.roles), ), }; }; const getUserById = async ({ params }, { userRepository }) => { return { body: await userRepository.findOneById(params.id), }; }; const insertUser = async ( req, { teamRepository, userRepository, roleRepository }, ) => { try { await validate('insertUser', req.body); const existingUser = await userRepository.findOne({ username: req.body.username, }); if (existingUser) { throw new Error(`User: ${existingUser.username} already exists.`); } const team = await teamRepository.findOneById(req.body.teamId); const userRole = await roleRepository.findOne({ name: 'User' }); const password = await bcrypt.hash(req.body.password); const user = new User(); user.username = req.body.username; user.passwordHash = password; user.team = team; user.roles = [userRole]; await userRepository.insert(user); const amount = await userRepository.count(); return { status: 201, body: { message: `Amount of users: ${amount}` }, }; } catch (error) { return { status: 400, body: { error: error.message }, }; } }; const updateUser = async ({ body }, { userRepository }) => { try { await validate('updateUser', body); const user = await userRepository.findOneById(body.id); user.username = body.username; await userRepository.save(user); return { body: { message: `user id: ${body.id} has new name: ${body.username}` }, }; } catch (error) { return { status: 400, body: { error: error.message }, }; } }; const deleteUser = async ({ params }, { userRepository }) => { return { body: await userRepository.removeById(params.id), }; }; export { UserDto, getAllUsers, getUserById, insertUser, updateUser, deleteUser, }; ================================================ FILE: packages/hadron-demo/tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "target": "es6", "noImplicitAny": true, "noEmitOnError": true, "moduleResolution": "node", "sourceMap": true, "outDir": "dist", "baseUrl": ".", "paths": { "*": ["./*"] }, "emitDecoratorMetadata": true, "experimentalDecorators": true, "inlineSources": true, "lib": ["es2017", "dom"] }, "include": ["src/**/*.*", "index.ts", "LICENSE"], "exclude": ["node_modules", "**/__tests__/**", "**/*/*.d.ts"] } ================================================ FILE: packages/hadron-demo/typeorm-demo/index.ts ================================================ import { ConnectionOptions } from 'typeorm'; import { User } from '../entity/User'; import { Team } from '../entity/Team'; import { Role } from '../entity/Role'; const connection: ConnectionOptions = { name: 'mysql-connection', type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'my-secret-pw', database: 'test', entities: [User, Team, Role], synchronize: true, }; export default { connection, entities: [User, Team, Role], }; ================================================ FILE: packages/hadron-error-handler/LICENSE ================================================ MIT License Copyright (c) 2018 Brainhub Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/hadron-error-handler/README.md ================================================ # Default Error for Hadron (Currently there is nothing to see here) ================================================ FILE: packages/hadron-error-handler/index.ts ================================================ import HadronError from './src/errorHandler'; export { HadronError as default }; ================================================ FILE: packages/hadron-error-handler/package.json ================================================ { "name": "@brainhubeu/hadron-error-handler", "version": "1.0.0", "description": "Error handler for hadron", "main": "dist/index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "prepare": "tsc" }, "files": [ "dist", "./LICENSE" ], "keywords": [ "error", "hadron" ], "author": "Brainhub", "license": "MIT", "publishConfig": { "access": "public" } } ================================================ FILE: packages/hadron-error-handler/src/errorHandler.ts ================================================ class HadronError extends Error { public error?: Error; constructor(message: string = 'Hadron unhandled error') { super(message); this.stack = new Error().stack; } } export default HadronError; ================================================ FILE: packages/hadron-error-handler/tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "target": "es6", "noImplicitAny": true, "noEmitOnError": true, "moduleResolution": "node", "sourceMap": true, "outDir": "dist", "baseUrl": ".", "paths": { "*": ["./*"] }, "emitDecoratorMetadata": true, "experimentalDecorators": true, "inlineSources": true, "lib": ["es2017", "dom"] }, "include": ["src/**/*.*", "index.ts", "LICENSE"], "exclude": ["node_modules", "**/__tests__/**", "**/*/*.d.ts"] } ================================================ FILE: packages/hadron-events/LICENSE ================================================ MIT License Copyright (c) 2018 Brainhub Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/hadron-events/README.md ================================================ ## Installation ```bash npm install @brainhubeu/hadron-events --save ``` [More info about installation](/core/#installation) ## Overview Event Manager is a tool which allows manipulating Hadron's default behavior without the need to change the code base. It can be achieved via custom listeners defined by the developer. There are a bunch of extension points spread all over the hadron framework where listeners can be hooked up. ## Initializing Pass package as an argument for hadron bootstrapping function: ```javascript const hadronEvents = require('@brainhubeu/hadron-events'); // ... importing and initializing other components hadron(expressApp, [hadronEvents], config).then(() => { console.log('Hadron with eventManager initialized'); }); ``` After initialization you can retrieve event manager from DI container - it is registered under the key `eventManager`. ## Event Manager methods ### Registering listeners for events ```javascript eventManager.registerEvents(listeners); ``` * `listeners` - an array of objects which have to follow convention showed below: ```javascript { name: 'string', // listener name event: 'string', // event to register to handler: 'function' // function to handle the event } ``` Example: ```javascript const config = { events: { listeners: [ { name: 'Listener1', event: 'createRoutesEvent', handler: (callback, ...args) => { const myCustomCallback = () => { console.log("Hey! I've changed the original hadron function!"); return callback(...args); }; return myCustomCallback(); }, }, { name: 'Listener2', event: 'myCustomEvent', handler: (callback, ...args) => { const myCustomCallback = () => { console.log('My custom event!'); return callback(...args); }; return myCustomCallback(); }, }, ], }, }; hadron(app, [hadronEvents], config).then((container) => { container.take('eventManager').emitEvent('myCustomEvent'); // "My custom event!" }); ``` ### Emitting events ```javascript eventEmitter.emitEvent(eventName); ``` Calls all listeners handlers registered for the event with event name passed to it. * `eventName` - name of the event which will be fired ## Listeners You can create your listeners in the main config file. As a first argument listener's handler method will receive a callback function originally called by hadron, so you can change/override it however you want and then return a call of newly created function or a call of existing callback if you don't want to change it. To be able to receive callback mentioned above, the first argument should be named exactly `callback`, otherwise, you will not receive the callback. You can also, define your listener's handler without `callback` argument or even without any arguments, which is also a valid way to create listeners, you just won't be able to access the callback. The second argument of listeners handler method is `...args`, which can be used as arguments for the callback function. An example of a listener: ```javascript { name: 'Listener', event: 'createRoutesEvent', handler: (callback, ...args) => { const myCustomCallback = () => { console.log("Hey! I've changed the original hadron function!"); return callback(...args); } return myCustomCallback(); } } ``` ## Extension points in hadron As said before, there are a couple of extension points in the hadron framework to which you can hook up your listeners. The extension depends from packages that You are using and are listed below: --- hadron-express `HANDLE_REQUEST_CALLBACK_EVENT` Event fires, before route callback function is called, passes route callback to the listener. Example: ```javascript const ExpressEvent = require('@brainhubeu/hadron-express').Event; const listeners = [ { name: 'Listener', event: ExpressEvent.HANDLE_REQUEST_CALLBACK_EVENT, // or simply event: 'HANDLE_REQUEST_CALLBACK_EVENT' handler: (callback, ...args) => { console.log('Request Handled!'); callback(...args); }, }, ]; ``` --- `HANDLE_TERMINATE_APPLICATION_EVENT` Event fires when the application is terminated with CTRL + C, passes default hadron callback to the listener. ```javascript const Event = require('@brainhubeu/hadron-events').Event; const listeners = [ { name: 'Listener', event: Event.HANDLE_TERMINATE_APPLICATION_EVENT, // or simply event: 'HANDLE_TERMINATE_APPLICATION_EVENT' handler: () => { console.log('Application is going to close'); }, }, ]; ``` ================================================ FILE: packages/hadron-events/index.ts ================================================ import { EventEmitter } from 'events'; import { Lifecycle, IContainer } from '@brainhubeu/hadron-core'; import eventManagerProvider from './src/eventManagerProvider'; import { IHadronEventsConfig } from './src/types'; import registerProcessEvents from './src/registerProcessEvents'; export * from './src/types'; export * from './src/constants'; export default eventManagerProvider; export const register = ( container: IContainer, config: IHadronEventsConfig, ) => { if (container.take('eventEmitter') === null) { container.register('eventEmitter', EventEmitter, Lifecycle.Singleton); } const eventManager = eventManagerProvider( container.take('eventEmitter'), config.events, ); eventManager.registerEvents(config.events.listeners); container.register('eventManager', eventManager); registerProcessEvents(eventManager); }; ================================================ FILE: packages/hadron-events/package.json ================================================ { "name": "@brainhubeu/hadron-events", "version": "1.0.1", "description": "Hadron event emitter module", "main": "dist/index.js", "files": [ "dist", "./LICENSE" ], "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "prepare": "tsc" }, "keywords": [ "hadron", "brainhub", "hadron-events" ], "author": "Brainhub", "license": "MIT", "publishConfig": { "access": "public" }, "dependencies": { "@brainhubeu/hadron-core": "^1.0.0", "@brainhubeu/hadron-utils": "^1.0.0" }, "devDependencies": { "@types/events": "1.2.0" } } ================================================ FILE: packages/hadron-events/src/__tests__/eventManagerProvider.ts ================================================ import { expect } from 'chai'; import { EventEmitter } from 'events'; import * as sinon from 'sinon'; import { IEventListener, IEventsConfig } from '../types'; import eventManagerProvider from '../eventManagerProvider'; describe('events registration', () => { let emitter: EventEmitter = null; beforeEach(() => { emitter = new EventEmitter(); }); afterEach(() => { emitter = null; }); it('throws an error if eventName is either null or empty string', () => { const listeners: IEventListener[] = [ { name: 'my-listener-1', event: '', // event to listen to handler: (callback, ...args) => { return callback(...args); }, }, { name: 'my-listener-2', event: 'someEvent', handler: (callback, ...args) => { return callback(...args); }, }, { name: 'my-listener-3', event: 'someEvent', // event to listen to handler: (callback, ...args) => { return callback(...args); }, }, ]; const config = {} as IEventsConfig; const eventManager = eventManagerProvider(emitter, config); expect(() => eventManager.registerEvents(listeners)).to.throw(); }); it('registers listeners', () => { const spy1 = () => sinon.spy(); const spy2 = (callback: any, ...args: any[]) => sinon.spy(); const listeners = [ { name: 'my-listener-1', event: 'someEvent', // event to listen to handler: spy1, }, { name: 'my-listener-2', event: 'someEvent', handler: spy2, }, ]; const eventManager = eventManagerProvider(emitter, { listeners }); eventManager.registerEvents(listeners); expect(emitter.listeners('someEvent').length).to.equal(2); expect(emitter.listeners('someEvent')[0]).to.equal(spy1); expect(emitter.listeners('someEvent')[1]).to.equal(spy2); }); }); ///////////////////////////////////////////////////// describe('events emitting', () => { let emitter: EventEmitter = null; let eventManager: any = null; beforeEach(() => { emitter = new EventEmitter(); }); afterEach(() => { emitter = null; eventManager = null; }); it('throws error when eventName argument is either null or empty string', () => { const listeners: IEventListener[] = [ { name: 'my-listener-1', event: 'someEvent', // event to listen to handler: () => { return 'test'; }, }, { name: 'my-listener-2', event: 'someEvent', handler: () => { return 'test'; }, }, { name: 'my-listener-3', event: 'changeCallbackEvent', // event to listen to handler: (callback, ...args) => { const newCallback = (...args: any[]) => { return 'changed'; }; return newCallback(...args); }, }, ]; const config = {} as IEventsConfig; eventManager = eventManagerProvider(emitter, config); eventManager.registerEvents(listeners); const callback = () => 'test'; expect(() => eventManager.emitEvent('', callback)).throw(); }); it('calls emitter.listeners with eventName argument', () => { const listeners: IEventListener[] = []; const config = {} as IEventsConfig; eventManager = eventManagerProvider(emitter, config); eventManager.registerEvents(listeners); const eventName = 'someEvent'; const listenersMethodSpy = sinon.spy(emitter, 'listeners'); const callback = () => 'test'; eventManager.emitEvent(eventName, callback); expect(listenersMethodSpy.alwaysCalledWithExactly(eventName)).to.equal( true, ); }); it('returns new callback function based on event listeners', () => { const listeners: IEventListener[] = [ { name: 'my-listener-3', event: 'changeCallbackEvent', // event to listen to handler: (callback, ...args) => { const newCallback = (...args: any[]) => { return 'changed'; }; return newCallback(...args); }, }, ]; const config = {} as IEventsConfig; eventManager = eventManagerProvider(emitter, config); eventManager.registerEvents(listeners); const callback = () => 'original function'; const cb = eventManager.emitEvent('changeCallbackEvent', callback); expect(cb()).to.equal('changed'); }); it('calls listeners handlers without "callback" argument', () => { const spy1 = sinon.spy(); const spy2 = sinon.spy(); const listenersWithoutCallback: IEventListener[] = [ { name: 'my-listener-1', event: 'someEvent', // event to listen to handler: (callback, ...args) => { return callback(...args); }, }, { name: 'my-listener-2', event: 'someEvent', handler: () => spy2(), }, ]; eventManager = eventManagerProvider(emitter, { listeners: listenersWithoutCallback, }); eventManager.registerEvents(listenersWithoutCallback); const callback = () => 'test'; eventManager.emitEvent('someEvent', callback)(); return expect(spy1.calledOnce) && expect(spy2.calledOnce); }); it('works if handler returns callback call', () => { const callback = () => 'test'; const listeners: IEventListener[] = [ { name: 'my-listener-1', event: 'someEvent', // event to listen to handler: (callback, ...args) => { return callback(...args); }, }, ]; const config = {} as IEventsConfig; eventManager = eventManagerProvider(emitter, config); eventManager.registerEvents(listeners); const eventFunc = eventManager.emitEvent('someEvent', callback); expect(eventFunc()).to.equal(callback()); }); it('does not throw an error when callback parameter is not passed', () => { const listeners: IEventListener[] = [ { name: 'my-listener-1', event: 'someEvent', // event to listen to handler: () => { return null; }, }, ]; const config = {} as IEventsConfig; eventManager = eventManagerProvider(emitter, config); eventManager.registerEvents(listeners); expect(eventManager.emitEvent('someEvent')).to.not.throw(); }); }); ================================================ FILE: packages/hadron-events/src/constants.ts ================================================ export enum Event { HANDLE_TERMINATE_APPLICATION_EVENT = 'HANDLE_TERMINATE_APPLICATION_EVENT', } ================================================ FILE: packages/hadron-events/src/eventManagerProvider.ts ================================================ import { hasFunctionArgument } from './helpers/functionHelper'; import { IEventEmitter, IEventListener, CallbackEvent, IEventsConfig, IEventManager, } from './types'; /** * Provider function to inject emitter and config into variable scope * @param emitter event emitter * @param config config parameters */ const eventManagerProvider = (emitter: IEventEmitter, config: IEventsConfig) => ({ registerEvents: (listeners: IEventListener[]) => { listeners.forEach((listener: IEventListener) => { if (listener.event === '' || listener.event === null) { throw new Error('eventName can not be empty'); } emitter.on(listener.event, listener.handler); }); }, emitEvent: (eventName: string, callback: CallbackEvent) => { if (eventName === '' || eventName === null) { throw new Error('eventName can not be empty'); } if (callback === undefined || callback === null) { callback = () => null; } return emitter .listeners(eventName) .reduce((prevCallback, currentHandler) => { // is first argument called "callback?" if (!hasFunctionArgument(currentHandler, 'callback')) { return (...args: any[]) => { currentHandler(...args); // manually run callback return prevCallback(...args); }; } return (...args: any[]) => currentHandler(prevCallback, ...args); }, callback); }, } as IEventManager); export default eventManagerProvider; ================================================ FILE: packages/hadron-events/src/helpers/functionHelper.ts ================================================ import { getArgs } from '@brainhubeu/hadron-utils'; // tslint:disable-next-line:ban-types function hasFunctionArgument(func: (args: any) => any, argumentName: string) { return getArgs(func).indexOf(argumentName) >= 0; } export { hasFunctionArgument }; ================================================ FILE: packages/hadron-events/src/registerProcessEvents.ts ================================================ import { IEventManager } from './types'; import { Event } from './constants'; export default (eventEmitter: IEventManager) => { process.on('exit', () => { eventEmitter.emitEvent(Event.HANDLE_TERMINATE_APPLICATION_EVENT); }); }; ================================================ FILE: packages/hadron-events/src/types.ts ================================================ export type CallbackEvent = (...args: any[]) => any; export type EventHandler = (callback: CallbackEvent, ...args: any[]) => any; export interface IEventEmitter { listeners: (event: string) => any[]; on: (eventName: string, handler: EventHandler) => void; emit: (eventName: string, event: object) => void; } export interface IEventListener { name: string; event: string; handler: EventHandler; } export interface IHadronEventsConfig { events: IEventsConfig; } export interface IEventsConfig { listeners: IEventListener[]; } export interface IEventManager { registerEvents: (listeners: IEventListener[]) => null; emitEvent: (eventName: string, callback?: CallbackEvent) => null; } ================================================ FILE: packages/hadron-events/tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "target": "es6", "noImplicitAny": true, "noEmitOnError": true, "moduleResolution": "node", "sourceMap": true, "outDir": "dist", "baseUrl": ".", "paths": { "*": ["./*"] }, "emitDecoratorMetadata": true, "experimentalDecorators": true, "inlineSources": true, "lib": ["es2017", "dom"] }, "include": ["src/**/*.*", "index.ts", "LICENSE"], "exclude": ["node_modules", "**/__tests__/**", "**/*/*.d.ts"] } ================================================ FILE: packages/hadron-express/LICENSE ================================================ MIT License Copyright (c) 2018 Brainhub Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/hadron-express/README.md ================================================ ## Installation INFO: Currently routing with hadron works only with the Express framework. ```bash npm install @brainhubeu/hadron-express --save ``` [More info about installation](/core/#installation) ## Express integration We need to include `hadron-express` package while initializing hadron. ```javascript const express = require('express'); const bodyParser = require('body-parser'); const port = process.env.PORT || 8080; const expressApp = express(); expressApp.use(bodyParser.json()); hadron(expressApp, [require('../hadron-express')], config).then((container) => { expressApp.listen(port); }); ``` ## Basic routing setup To set up routes with Hadron, we are able to include them as objects in config object under key `routes`. ```javascript const config = { routes: { helloWorldRoute: { callback: () ='Hello world !', methods: ['GET'], path: '/', }, }, }; ``` Basic, required structure of route config object includes: * `callback` - function called when request is made, returned value will be send as a response (except if you call `res` methods directly) * `methods` - array of [HTTP methods](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods) * `path` - route path ## Callback The callback function can take route parameters as an arguments. Hadron also allows us to grab a container value easily. ```javascript routeWithParam: { callback: (firstParam) => `firstParam value: ${firstParam}`, methods: ['GET'], path: '/:firstParam', } ``` Using this simple example, if we send a request, for example `http://localhost/foobar` will provide a response as below: ```json "firstParam value: foobar" ``` --- When you would like to implement multiple route parameters, their order as arguments in callback does not matter, argument name needs only to match parameter name. ```javascript multipleParams: { callback: (secondParam, firstParam) =`${firstParam} ${secondParam}`, methods: ['GET'], path: '/:firstParam/:secondParam', } ``` GET request with path: `http://localhost/Hello/World` will result with following response: ```json "Hello World" ``` ### Locals (available from 2.0.0) As a third parameter, hadron delivers `locals` from your response. You can inject its content in middlewares, e.g. ```javascript const route = { callback: (req, container, locals) => locals.testValue, middlewares: [ (req, res, next) => { res.locals.testValue = 'I am test!'; next(); }, ], }; ``` ## Retrieving items from container in callback Callback function provides a simple way to retrieve items from container with ease. Simply set item's key as callback function's argument. Let's see an example below: ```javascript hadron(expressApp, [require('../hadron-express')], { routes: { routeWithContainerValue: { // sayHello argument will refer to container value callback: (sayHello = `hadron says: ${sayHello}`), methods: ['GET'], path: '/', }, }, }).then((container) => { // Register value under key sayHello container.register('sayHello', 'Hello World'); }); ``` After sending a request to the `/` path, the response will look like that: ```json "hadron says: Hello World" ``` --- Hadron will first look for request parameters and next if not found any, it will look for value in the container. So if you register a key `foo` in a container and set the route param under the same name, it will inject param's value into callback's argument foo. ```javascript container.register('foo', 'container'); ``` ```javascript exampleRoute: { callback: (foo) =`foo value: ${foo}`, methods: ['GET'], path: '/:foo', }, ``` Response for `GET` request _/param_ will look like this: ```json "foo value: param" ``` ## Middlewares _Note: Currently middlewares only refer to express._ Routing with Hadron provides a middleware support. You need to pass array with middleware functions to a `middleware` key in route config. For example: ```javascript middlewareExample: { callback: () => { console.log('Callback function'); }, methods: ['GET'], middleware: [ (req, res, next) => { console.log(`First middleware`); next(); }, (req, res, next) => { console.log(`Second middleware`); next(); }, ], path: '/', }, ``` `GET` request to `/` will log to the console following: ```sh First middleware Second middleware Callback function ``` Middlewares take three arguments: `request`, `response` and `next`. First two are objects and third one - function which executed continues request flow. You can read more about middlewares in [express guide](https://expressjs.com/en/guide/using-middleware.html) ## Routes nesting (available from 2.0.0) In case of more complicated routing, hadron-express offers possibility to nest routes. That way, You can specify bunch of route properties, that all child routes will inherit. Route properties that can be inherited: * path (will add parent's path beforehand new path), * middlewares * methods, ```javascript const nestedRoute = { middleware: [myTestMiddleware], method: ['GET'], path: '/test', routes: { route1: { // path here is going to be /test/test1/, it's going to have 'GET' method on default and middleware myTestMiddleware will be called before path: '/test1', callback: () => 'It works! Trust me...', }, route1: { // path here is going to be /test/test2/, it's going to have 'GET' and 'POST' methods on default and middlewares myCustomMiddleware and myTestMiddleware will be called before path: '/test2', method: ['POST'], middleware: [myCustomMiddleware], callback: () => 'It works! Trust me...', }, }, }; ``` If You would like to override parent property, just define new one with `$` sign before, e.g. `$middlewares`, `$path`, `$method`. ```javascript const nestedRoute = { middleware: [myTestMiddleware], method: ['GET'], path: '/test', routes: { route1: { // path here is going to be /test1/, it's going to have 'GET' method on default and middleware myTestMiddleware will be called before $path: '/test1', callback: () => 'It works! Trust me...', }, route1: { // path here is going to be /test/test2/, it's going to have 'GET' and 'POST' methods on default and no middlewares path: '/test2', method: ['POST'], $middleware: [], callback: () => 'It works! Trust me...', }, }, }; ``` You can define nested route endlessly, all of them will inherit properties of it's parent and other ancestors (of course if they were not overwritten with `$` sign). ```javascript const nestedRoute = { middleware: [myTestMiddleware], method: ['GET'], path: '/test', routes: { route1: { // path here is going to be /test/test2/, it's going to have 'GET' and 'POST' methods on default and no middlewares path: '/test2', method: ['POST'], $middleware: [], callback: () => 'It works! Trust me...', routes: { // path here is going to be /test/test2/deep/, it's going to have 'GET' and 'POST' methods on default and no middlewares deepRoute1: { path: 'deep', callback: () => 'The Dwarves delved too greedily and too deep', }, }, }, }, }; ``` ================================================ FILE: packages/hadron-express/index.ts ================================================ import hadronExpress from './src/hadronToExpress'; export { Callback, IRoutesConfig, IRoute, Middleware, IContainer, IHadronExpressConfig, } from './src/types'; import { IRoutesConfig, IContainer, IHadronExpressConfig, RoutePathsConfig, } from './src/types'; export { Event } from './src/constants/eventNames'; export default hadronExpress; export const register = (container: IContainer, config: IHadronExpressConfig) => hadronExpress( { routes: config.routes as IRoutesConfig, routePaths: config.routePaths as RoutePathsConfig, }, container, ); ================================================ FILE: packages/hadron-express/package.json ================================================ { "name": "@brainhubeu/hadron-express", "version": "2.0.0", "description": "Hadron module implementing express elements", "main": "dist/index.js", "files": [ "dist", "LICENSE" ], "directories": { "test": "tests" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "prepare": "tsc" }, "keywords": [ "hadron", "hadron-express" ], "author": "Brainhub", "license": "MIT", "dependencies": { "@brainhubeu/hadron-core": "^1.0.0", "@brainhubeu/hadron-error-handler": "^1.0.0", "@brainhubeu/hadron-events": "^1.0.1", "@brainhubeu/hadron-json-provider": "^1.0.0", "@brainhubeu/hadron-utils": "^1.0.0", "express": "4.16.2", "http-status": "1.1.0" }, "devDependencies": { "@types/express": "4.11.1", "@types/http-status": "0.2.30", "@types/multer": "1.3.6", "@types/ramda": "0.25.23", "@types/supertest": "2.0.4", "multer": "1.3.0", "ramda": "0.25.0", "supertest": "3.0.0" }, "publishConfig": { "access": "public" } } ================================================ FILE: packages/hadron-express/src/__tests__/__mocks__/routes/route.js ================================================ routes = () => ({ testRoute: { callback: () => null, methods: ['GET'], path: '/', }, }); module.exports = routes; ================================================ FILE: packages/hadron-express/src/__tests__/createContainerProxy.ts ================================================ import { Container } from '@brainhubeu/hadron-core'; import createContainerProxy from '../createContainerProxy'; import * as sinon from 'sinon'; import { expect } from 'chai'; describe('proxy container', () => { it('calls keys() method on the container ', () => { const keys = sinon.spy(Container, 'keys'); const containerProxy = createContainerProxy(Container); containerProxy.keys(); expect(keys.calledOnce).to.be.equal(true); }); it('calls take() method on the container', () => { Container.register('key', 'item'); const take = sinon.spy(Container, 'take'); const containerProxy = createContainerProxy(Container); // tslint:disable-next-line containerProxy.key; expect(take.calledOnce).to.be.equal(true); }); }); ================================================ FILE: packages/hadron-express/src/__tests__/generateMiddlewares.ts ================================================ import { expect } from 'chai'; import { isRawMiddleware, createRawMiddleware } from '../generateMiddlewares'; import { getArgs } from '@brainhubeu/hadron-utils'; import * as sinon from 'sinon'; import * as express from 'express'; describe('isRawMiddleware', () => { it('returns true when passed middleware is Express middleware', () => { const middleware = (req, res, next) => { next(); }; const actual = isRawMiddleware(middleware); expect(actual).to.equal(true); }); it('returns false when passed middleware is Hadron middleware', () => { const middleware = (req, deps) => { return; }; const actual = isRawMiddleware(middleware); expect(actual).to.equal(false); }); }); describe('createRawMiddleware', () => { it('creates Express middleware from Hadron Middleware', () => { const hadronMiddleware = (req, deps) => { return null; }; const containerProxy = {}; const expressMiddleware = createRawMiddleware( hadronMiddleware, containerProxy, ); expect(getArgs(expressMiddleware as any)).to.eql(['req', 'res', 'next']); }); it('creates middleware that sends response', async () => { const hadronMiddleware = (req, deps) => { return { status: 201, body: 'hello', }; }; const containerProxy = {}; const expressMiddleware = createRawMiddleware( hadronMiddleware as any, containerProxy, ); const req = {}; const res = { status: sinon.spy(), json: sinon.spy(), }; const next = sinon.spy(); await expressMiddleware(req as express.Request, res as any, next); expect(res.status.firstCall.args).to.eql([201]); expect(res.json.firstCall.args).to.eql(['hello']); }); it('creates middleware that sets response headers and status without sending response', async () => { const hadronMiddleware = (req, deps) => { return { type: 'PARTIAL_RESPONSE', status: 300, headers: { 'custom-header': 'foo', }, }; }; const containerProxy = {}; const expressMiddleware = createRawMiddleware( hadronMiddleware as any, containerProxy, ); const req = {}; const res = { status: sinon.spy(), json: sinon.spy(), set: sinon.spy(), }; const next = sinon.spy(); await expressMiddleware(req as express.Request, res as any, next); expect(res.status.firstCall.args).to.eql([300]); expect(res.set.firstCall.args).to.eql(['custom-header', 'foo']); expect(res.json.called).to.equal(false); }); it('creates middleware that modifies request', async () => { const hadronMiddleware = (req, deps) => { return { type: 'PARTIAL_REQUEST', values: { body: 'bar', customKey: 'foo', }, }; }; const containerProxy = {}; const expressMiddleware = createRawMiddleware( hadronMiddleware as any, containerProxy, ); const req = { body: null, existingKey: 'baz', }; const res = {}; const next = sinon.spy(); await expressMiddleware(req as any, res as any, next); expect(req).to.eql({ body: 'bar', customKey: 'foo', existingKey: 'baz', }); }); }); ================================================ FILE: packages/hadron-express/src/__tests__/hadronToExpress.ts ================================================ import { expect } from 'chai'; import * as express from 'express'; import * as fs from 'fs-extra'; import * as HTTPStatus from 'http-status'; import * as multer from 'multer'; import * as R from 'ramda'; import * as sinon from 'sinon'; import * as request from 'supertest'; import { Container } from '@brainhubeu/hadron-core'; import { json as bodyParser } from 'body-parser'; import NoRouterMethodSpecifiedError from '../errors/NoRouterMethodSpecifiedError'; import routesToExpress, { prepareMiddlewares, preparePath, prepareMethods, } from '../hadronToExpress'; import { Event } from '../constants/eventNames'; import { IRequest, IResponseSpec } from '../types'; let app = express(); const createTestRoute = ( path: string, methods: string[], callback: (req: IRequest, dependencies: any) => IResponseSpec, middleware?: Array<(req: any, res: any, next: any) => any>, ) => ({ routes: { testRoute: { callback, methods, middleware, path, }, }, }); const getRouteProp = (expressApp: any, prop: string) => expressApp._router.stack // registered routes .filter((r: any) => r.route) // take out all the middleware .map((r: any) => r.route[prop]); // get all the props describe('router config', () => { beforeEach(() => { app = express(); app.use(bodyParser()); Container.register('server', app); Container.register('eventManager', null); }); describe('generating routes', () => { it('should generate express route based on config file', () => { const testRoute = createTestRoute('/index', ['GET'], () => null); return routesToExpress(testRoute, Container).then(() => expect(getRouteProp(app, 'path')[0]).to.equal('/index'), ); }); it('should generate correct router method based on config file', () => { const testRoute = createTestRoute('/index', ['POST'], () => null); return routesToExpress(testRoute, Container).then(() => expect(getRouteProp(app, 'methods')[0].post).to.equal(true), ); }); it('returns status OK for request from generated route', () => { const testRoute = createTestRoute('/testRequest', ['GET'], () => null); routesToExpress(testRoute, Container); return request(app) .get('/testRequest') .expect(HTTPStatus.OK); }); it('throws a NoRouterMethodSpecifiedError if no methods were specified', () => { const testRoute = createTestRoute('/index', [], () => null); return routesToExpress(testRoute, Container).catch((error) => expect(error).to.be.instanceOf(NoRouterMethodSpecifiedError), ); }); it('generate multiple methods based on config', () => { const callback = (req: any, res: any) => req.params.testParam + req.params.anotherParam; const middle = sinon.spy(); const testRoute = createTestRoute( '/testRoute', ['PUT', 'DELETE'], callback, [middle], ); routesToExpress(testRoute, Container).then( () => expect(getRouteProp(app, 'methods')[0].put).to.equal(true) && expect(getRouteProp(app, 'methods')[1].delete).to.equal(true), ); }); it('should return HTTP Status 500 if callback is not defined', () => { const testRoute = createTestRoute('/testRoute', ['GET'], null); routesToExpress(testRoute, Container); return request(app) .get(`/testRoute`) .expect(HTTPStatus[500]); }); it('calls emitEvent method', () => { const eventManager = { emitEvent: sinon.spy(), }; Container.register('eventManager', eventManager); const testRoute = createTestRoute('/index', ['GET'], () => null); return routesToExpress(testRoute, Container).then(() => request(app) .get(`/index`) .then(() => expect( eventManager.emitEvent.calledWith( Event.HANDLE_REQUEST_CALLBACK_EVENT, ), ).to.equal(true), ), ); }); it('calls the response event if eventManager is present', () => { const eventManager = { emitEvent: sinon.spy(() => () => null), }; Container.register('eventManager', eventManager); const testRoute = createTestRoute('/index', ['GET'], () => null); return routesToExpress(testRoute, Container).then(() => request(app) .get('/index') .then(() => { expect( eventManager.emitEvent.calledWith(Event.HANDLE_RESPONSE_EVENT), ).to.equal(true); }), ); }); it('should load routes using json-provider and load them', () => { routesToExpress( { routePaths: [ [ './packages/hadron-express/src/__tests__/__mocks__/routes/*', 'js', ], ], }, Container, ); return request(app) .get(`/`) .then(() => { expect(getRouteProp(app, 'path')[0]).to.equal('/'); }); }); }); describe('routes nesting', () => { describe('prepareMiddlewares()', () => { it('should return parents middlewares', () => { const middleware = () => null; const result = prepareMiddlewares([], null, [middleware]); expect(result).to.contain(middleware); }); it('should return joined middlewares of parent and current route', () => { const middleware = () => null; const middleware2 = () => null; const result = prepareMiddlewares([middleware2], null, [middleware]); expect(result).to.have.members([middleware, middleware2]); }); it('should exclude parent and route middleware, if $middlewares exists', () => { const middleware = () => null; const middleware2 = () => null; const middleware3 = () => null; const result = prepareMiddlewares( [middleware2], [middleware3], [middleware], ); expect(result).to.not.have.members([middleware2, middleware]); }); it('should contain $middlewares, if they exists', () => { const middleware = () => null; const middleware2 = () => null; const middleware3 = () => null; const result = prepareMiddlewares( [middleware2], [middleware3], [middleware], ); expect(result).to.have.members([middleware3]); }); }); describe('preparePath()', () => { it('should return just a path of route', () => { expect(preparePath('path', null, '')).to.equal('path'); }); it('should return merged parent path and route path', () => { expect(preparePath('path', null, 'previousPath')).to.equal( 'previousPath/path', ); }); it('should overwrite parent path to $path', () => { expect(preparePath('path', 'path2', 'previousPath')).to.equal('path2'); }); }); describe('prepareMethods()', () => { it('should return methods of route', () => { expect(prepareMethods(['GET'], null, [])).to.have.members(['GET']); }); it('should return join methods of route and parent route', () => { expect(prepareMethods(['GET'], null, ['POST'])).to.have.members([ 'GET', 'POST', ]); }); it('should unique methods of route and parent route', () => { expect(prepareMethods(['GET'], null, ['POST', 'GET'])).to.be.length(2); }); }); }); describe('router params', () => { it('should pass parameter to callback func - request param', () => { const callback = ({ params }) => params.valueA; const testParam = 'This is a test'; const testRoute = createTestRoute('/index/:valueA', ['GET'], callback); routesToExpress(testRoute, Container); return request(app) .get(`/index/${testParam}`) .expect(HTTPStatus.OK) .then((res: any) => { expect(res.body).to.equal(testParam); }); }); it('should pass parameter to callback func - query', () => { const callback = ({ query }) => query.valueA; const testParam = 'This is a test'; const testRoute = createTestRoute('/index', ['GET'], callback); routesToExpress(testRoute, Container); return request(app) .get(`/index?valueA=${testParam}`) .expect(HTTPStatus.OK) .then((res: any) => { expect(res.body).to.equal(testParam); }); }); it('should pass multiple parameters to callback func - params', () => { const callback = ({ params }) => params.valueA + params.valueB; const testParam = 'This is a test'; const secondParam = ' This is a second param'; const testRoute = createTestRoute( '/index/:valueA/:valueB', ['GET'], callback, ); routesToExpress(testRoute, Container); return request(app) .get(`/index/${testParam}/${secondParam}`) .expect(HTTPStatus.OK) .then((res: any) => expect(res.body).to.equal(testParam + secondParam)); }); it('should pass multiple parameters to callback func - query', () => { const callback = ({ query }) => query.valueA + query.valueB; const testParam = 'This is a test'; const secondParam = ' This is a second param'; const testRoute = createTestRoute('/index', ['GET'], callback); routesToExpress(testRoute, Container); return request(app) .get(`/index?valueA=${testParam}&valueB=${secondParam}`) .expect(HTTPStatus.OK) .then((res: any) => expect(res.body).to.equal(testParam + secondParam)); }); it('should pass query to callback func', () => { const callback = ({ query }) => query.foo; const testQuery = 'bar'; const testRoute = createTestRoute('/index', ['GET'], callback); routesToExpress(testRoute, Container); return request(app) .get(`/index/?foo=${testQuery}`) .expect(HTTPStatus.OK) .then((res) => { expect(res.body).to.equal(testQuery); }); }); it('should pass body to callback func', () => { const callback = ({ body }) => body.testData; const postData = { testData: 'some value', }; const testRoute = createTestRoute('/index', ['POST'], callback); routesToExpress(testRoute, Container); return request(app) .post(`/index`) .send(postData) .expect(HTTPStatus.OK) .then((res) => { expect(res.body).to.equal(postData.testData); }); }); }); describe('router middleware', () => { it('calls middleware passed in router config', () => { const callback = (): any => null; const spy = sinon.spy(); const middle = (req: any, res: any, next: any) => { spy(); next(); }; const testRoute = createTestRoute('/testRoute', ['GET'], callback, [ middle, ]); routesToExpress(testRoute, Container); return request(app) .get(`/testRoute`) .expect(HTTPStatus.OK) .then(() => expect(spy.called).to.be.eq(true)); }); it('calls multiple middlewares passed in router config', () => { const callback = (): any => null; const firstSpy = sinon.spy(); const secondSpy = sinon.spy(); const firstMiddleware = (req: any, res: any, next: any) => { firstSpy(); next(); }; const secondMiddleware = (req: any, res: any, next: any) => { secondSpy(); next(); }; const testRoute = createTestRoute('/testRoute', ['GET'], callback, [ firstMiddleware, secondMiddleware, ]); routesToExpress(testRoute, Container); return request(app) .get(`/testRoute`) .expect(HTTPStatus.OK) .then(() => { expect(firstSpy.called).to.be.eq(true); expect(secondSpy.called).to.be.eq(true); }); }); }); describe('file handling', () => { const upload = multer({ dest: `${__dirname}/testUploads` }); const callback = ({ files, file }) => ({ body: files || file }); const uploadMiddleware = (req: any, res: any, next: any) => upload.any()(req, res, next); after(() => { fs.remove(`${__dirname}/testUploads`); }); it('should save file passed to route', () => { const testRoute = createTestRoute('/testUploads', ['POST'], callback, [ uploadMiddleware, ]); routesToExpress(testRoute, Container); const mockDir = `${__dirname}/testUploads`; return request(app) .post('/testUploads') .attach('image', `${__dirname}/__mocks__/sample.jpeg`) .expect(HTTPStatus.OK) .then(() => { const files = fs.readdirSync(mockDir); expect(files.length).to.equal(1); }); }); it('should pass file to callback func', () => { const testRoute = createTestRoute('/testUpload', ['POST'], callback, [ uploadMiddleware, ]); routesToExpress(testRoute, Container); return request(app) .post('/testUpload') .attach('image', `${__dirname}/__mocks__/sample.jpeg`) .expect(HTTPStatus.OK) .then((res) => { expect(R.omit(['filename', 'path', 'size'], res.body[0])).to.eql({ destination: `${__dirname}/testUploads`, encoding: '7bit', fieldname: 'image', mimetype: 'image/jpeg', originalname: 'sample.jpeg', }); }); }); }); }); ================================================ FILE: packages/hadron-express/src/constants/eventNames.ts ================================================ export enum Event { HANDLE_REQUEST_CALLBACK_EVENT = 'HANDLE_REQUEST_CALLBACK_EVENT', HANDLE_RESPONSE_EVENT = 'HANDLE_RESPONSE_EVENT', } ================================================ FILE: packages/hadron-express/src/constants/routing.ts ================================================ export enum HTTPRequestMethods { GET = 'GET', POST = 'POST', PATCH = 'PATCH', DELETE = 'DELETE', PUT = 'PUT', } ================================================ FILE: packages/hadron-express/src/createContainerProxy.ts ================================================ import { IContainer } from './types'; const createContainerProxy = (container: IContainer): any => { return new Proxy( { keys() { return container.keys(); }, }, { get(target: any, name) { if (typeof target[name] === 'function') { return target[name]; } return container.take(name as string); }, }, ); }; export default createContainerProxy; ================================================ FILE: packages/hadron-express/src/declarations.d.ts ================================================ declare module '@hadron/events'; declare module '@hadron/utils'; ================================================ FILE: packages/hadron-express/src/errors/CreateRouteError.ts ================================================ import HadronErrorHandler from '@brainhubeu/hadron-error-handler'; export default class CreateRouteError extends HadronErrorHandler { constructor(routeName: string, error: Error) { super(`Cannot create route ${routeName}.`); this.stack = error.stack; } } ================================================ FILE: packages/hadron-express/src/errors/GenerateMiddlewareError.ts ================================================ import HadronErrorHandler from '@brainhubeu/hadron-error-handler'; export default class GenerateMiddlewareError extends HadronErrorHandler { constructor(error: Error) { super(); this.name = 'GenerateMiddlewareError'; this.error = error; } } ================================================ FILE: packages/hadron-express/src/errors/InvalidRouteMethodError.ts ================================================ import HadronErrorHandler from '@brainhubeu/hadron-error-handler'; export default class InvalidRouteMethodError extends HadronErrorHandler { constructor(route: string, method: string) { super(); this.message = `Invalid route method '${method}' in ${route} route`; this.name = 'InvalidRouteError'; } } ================================================ FILE: packages/hadron-express/src/errors/NoRouterMethodSpecifiedError.ts ================================================ import HadronErrorHandler from '@brainhubeu/hadron-error-handler'; export default class NoRouterMethodSpecifiedError extends HadronErrorHandler { constructor(route: string) { super(); this.message = `No route methods were specified for ${route} route`; this.name = 'NoRouterMethodSpecifiedError'; } } ================================================ FILE: packages/hadron-express/src/generateMiddlewares.ts ================================================ import * as express from 'express'; import { Container } from '@brainhubeu/hadron-core'; import { IRoute, Middleware, HadronMiddleware } from './types'; import GenerateMiddlewareError from './errors/GenerateMiddlewareError'; import prepareRequest from './prepareRequest'; import handleResponseSpec from './handleResponseSpec'; import { getArgs } from '@brainhubeu/hadron-utils'; export const isRawMiddleware = (middleware: any) => { const [firstArg, secondArg, thirdArg] = getArgs(middleware); return ( (firstArg === 'req' || firstArg === 'request') && (secondArg === 'res' || secondArg === 'response') && thirdArg === 'next' ); }; export const createRawMiddleware = ( hadronMiddleware: HadronMiddleware, containerProxy: any, ) => { return async ( req: express.Request, res: express.Response, next: express.NextFunction, ) => { const hadronReq = prepareRequest(req); const result = await hadronMiddleware(hadronReq, containerProxy); switch (result.type) { case 'PARTIAL_REQUEST': { Object.assign(req, result.values); next(); break; } case 'PARTIAL_RESPONSE': { handleResponseSpec(res)(result); next(); break; } default: { handleResponseSpec(res)(result); } } }; }; const generateMiddlewares = ( middlewares: Middleware[], containerProxy: any, ) => { if (!middlewares) { return middlewares; } return middlewares.map((middleware: any) => { const rawMiddleware: Middleware = isRawMiddleware(middleware) ? middleware : createRawMiddleware(middleware as HadronMiddleware, containerProxy); return ( req: express.Request, res: express.Response, next: express.NextFunction, ) => { Promise.resolve() .then(() => rawMiddleware(req, res, next)) .catch((error) => { const logger = Container.take('hadronLogger'); if (logger) { logger.warn(new GenerateMiddlewareError(error)); } res.sendStatus(500); }); }; }); }; export default generateMiddlewares; ================================================ FILE: packages/hadron-express/src/hadronToExpress.ts ================================================ import * as express from 'express'; import * as nodePath from 'path'; import { IContainer, IRoute, Middleware, IHadronExpressConfig, IRoutesConfig, } from './types'; import { Event } from './constants/eventNames'; import CreateRouteError from './errors/CreateRouteError'; import createContainerProxy from './createContainerProxy'; import prepareRequest from './prepareRequest'; import generateMiddlewares from './generateMiddlewares'; import handleResponseSpec from './handleResponseSpec'; import jsonProvider from '@brainhubeu/hadron-json-provider'; const createRoutes = ( app: express.Application, route: IRoute, containerProxy: any, routeName: string, ) => { return route.methods.map((method: string) => { (app as any)[method.toLowerCase()]( route.path, ...route.middleware, (req: express.Request, res: express.Response) => { const request = prepareRequest(req); const eventManager = containerProxy.take('eventManager'); Promise.resolve() .then(() => { if (!eventManager) { return route.callback(request, containerProxy, res.locals); } const newRouteCallback = eventManager.emitEvent( Event.HANDLE_REQUEST_CALLBACK_EVENT, route.callback, ); return newRouteCallback(request, containerProxy, res.locals); }) .then((callback) => { if (!eventManager) { return handleResponseSpec(res)(callback); } const newResponseHandler = eventManager.emitEvent( Event.HANDLE_RESPONSE_EVENT, handleResponseSpec, ); return newResponseHandler(res)(callback); }) .catch((error) => { const logger = containerProxy.hadronLogger; const createRouteError = new CreateRouteError(routeName, error); if (logger) { logger.error(createRouteError); } res.sendStatus(500); }); }, ); }); }; const convertToExpress = ( config: IHadronExpressConfig, container: IContainer, ) => { const app = container.take('server'); const promises: Array> = []; const containerProxy = createContainerProxy(container); if (config.routes) { promises.push(Promise.resolve(config.routes)); } if (config.routePaths) { const paths: string[] = []; const extensions: string[] = []; (config.routePaths as any).forEach((path: string[]) => { paths.push(path[0]); if (path.length > 1) { extensions.push(path[1]); } }); promises.push(jsonProvider(paths, extensions)); } return ( Promise.all(promises) .then((results) => results.reduce( (accumulator, current) => ({ ...accumulator, ...current }), {}, ), ) // flatten routes and prepare all components .then((routes: any) => (Object as any) .keys(routes) .map((key: string) => prepareRoute(routes[key], key, containerProxy)) .reduce((accumulator: any, current: any) => ({ ...accumulator, ...current, })), ) .then((preparedRoutes: IRoutesConfig) => { (Object as any).keys(preparedRoutes).map((key: string) => { const route: IRoute = preparedRoutes[key]; createRoutes(app, route, container, key); }); }) ); }; const prepareRoute = ( route: IRoute, key: string, container: IContainer, parentRoute: IRoute = {}, parentKey: string = null, ) => { const middlewares = prepareMiddlewares( generateMiddlewares(route.middleware, container), generateMiddlewares(route.$middleware, container), parentRoute.middleware, ); const path = preparePath(route.path, route.$path, parentRoute.path); const methods = prepareMethods( route.methods, route.$methods, parentRoute.methods, ); const routeKey = parentKey ? `${parentKey}.${key}` : key; const preparedRoute = { ...route, path, methods, middleware: middlewares, }; const result = { [routeKey]: preparedRoute, }; if (route.routes) { return { ...result, ...(Object as any) .keys(route.routes) .map((childKey: string) => prepareRoute( (route.routes as any)[childKey], childKey, container, preparedRoute, routeKey, ), ) .reduce((accumulator: any, current: any) => ({ ...accumulator, ...current, })), }; } return result; }; export const prepareMiddlewares = ( middleware: any[], $middleware: any[], parentMiddleware: Middleware[], ) => { if ($middleware) { return $middleware; } return [...(parentMiddleware || []), ...(middleware || [])]; }; export const preparePath = ( path: string, $path: string, parentPath: string = '', ) => { if ($path) { return $path; } return nodePath.join(parentPath, path); }; export const prepareMethods = ( methods: string[], $methods: string[], parentMethods: string[] = [], ) => { if ($methods) { return $methods; } return [...(methods || []), ...parentMethods].reduce( (accumulator, next) => accumulator.indexOf(next) >= 0 ? accumulator : [...accumulator, next], [], ); }; export default convertToExpress; ================================================ FILE: packages/hadron-express/src/handleResponseSpec.ts ================================================ import * as express from 'express'; import * as HTTPStatus from 'http-status'; import { IResponseSpec, IPartialResponseSpec } from './types'; const getClass = (val: any) => Object.prototype.toString.call(val).slice(8, -1); const isPrimitive = (val: any) => { return ['String', 'Number', 'Null', 'Undefined'].includes(getClass(val)); }; const handleResponseSpec = (res: express.Response) => ( responseSpec: IResponseSpec | IPartialResponseSpec, ) => { if (isPrimitive(responseSpec)) { return res.json(responseSpec); } const status = responseSpec.status ? responseSpec.status : responseSpec.type === 'RESPONSE' && responseSpec.redirect ? HTTPStatus.FOUND : HTTPStatus.OK; const headers = responseSpec.headers || {}; Object.keys(headers).forEach((headerName) => { res.set(headerName, headers[headerName]); }); res.status(status); if (responseSpec.type === 'PARTIAL_RESPONSE') { return; } const body = responseSpec.body || {}; if (responseSpec.redirect) { return res.redirect(responseSpec.redirect); } if (responseSpec.view) { const { name, bindings } = responseSpec.view; return res.render(name, bindings); } res.json(body); }; export default handleResponseSpec; ================================================ FILE: packages/hadron-express/src/prepareRequest.ts ================================================ import * as express from 'express'; import { IRequest } from './types'; const prepareRequest = ( expressRequest: express.Request, locals = {}, ): IRequest => { return { locals, headers: expressRequest.headers, body: expressRequest.body, params: expressRequest.params, query: expressRequest.query, file: expressRequest.file, files: expressRequest.files, }; }; export default prepareRequest; ================================================ FILE: packages/hadron-express/src/types.ts ================================================ import * as express from 'express'; export type Middleware = ( req: express.Request, res: express.Response, next: express.NextFunction, ) => any; export type Callback = (...args: any[]) => any; export interface IRoute { callback?: Callback; middleware?: Middleware[]; $middleware?: Middleware[]; path?: string; $path?: string; methods?: string[]; $methods?: string[]; routes?: IRoutesConfig[]; } export interface IRoutesConfig { [key: string]: IRoute; } export type RoutePathsConfig = string[][]; export interface IContainer { take: (key: string) => any; keys: () => string[]; } export interface IHeaders { [headerName: string]: string; } export interface IView { name: string; bindings?: any; } export type StatusCode = | 100 | 101 | 102 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 304 | 305 | 307 | 308 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 451 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511; export interface IResponseSpec { type: 'RESPONSE'; status?: StatusCode; redirect?: string; headers?: IHeaders; body?: any; view?: IView; } export interface IPartialResponseSpec { type: 'PARTIAL_RESPONSE'; status?: StatusCode; headers?: IHeaders; } export interface IRequest { locals?: any; headers?: any; body?: any; params?: any[]; query?: any[]; file?: any; files?: any; } export interface IPartialRequest { type: 'PARTIAL_REQUEST'; values: { [key: string]: any; }; } export interface IHadronExpressConfig { routes?: IRoutesConfig; routePaths?: RoutePathsConfig; } export type MiddlewareResult = | IPartialResponseSpec | IPartialRequest | IResponseSpec; export type HadronMiddleware = ( request: IRequest, dependencies: any, ) => MiddlewareResult; ================================================ FILE: packages/hadron-express/tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "target": "es6", "noImplicitAny": true, "noEmitOnError": true, "moduleResolution": "node", "sourceMap": true, "outDir": "dist", "baseUrl": ".", "paths": { "*": ["./*"] }, "emitDecoratorMetadata": true, "experimentalDecorators": true, "inlineSources": true, "lib": ["es2017", "dom"] }, "include": ["src/**/*.*", "index.ts", "LICENSE"], "exclude": ["node_modules", "**/__tests__/**", "**/*/*.d.ts"] } ================================================ FILE: packages/hadron-file-locator/LICENSE ================================================ MIT License Copyright (c) 2018 Brainhub Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/hadron-file-locator/README.md ================================================ # Hadron file locator It's just part of hadron-json-provider ================================================ FILE: packages/hadron-file-locator/index.ts ================================================ import locate, { configLocate } from './src/file-locator'; export default locate; export { configLocate }; ================================================ FILE: packages/hadron-file-locator/package.json ================================================ { "name": "@brainhubeu/hadron-file-locator", "version": "1.0.0", "description": "Hadron module for searching file paths in directories", "main": "dist/index.js", "files": [ "dist", "./LICENSE" ], "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "prepare": "tsc" }, "keywords": [ "hadron", "brainhub", "hadron-file-locator" ], "author": "Brainhub", "license": "MIT", "dependencies": { "glob": "7.1.2" }, "devDependencies": { "@types/glob": "5.0.35" }, "publishConfig": { "access": "public" } } ================================================ FILE: packages/hadron-file-locator/src/__tests__/file-locator.ts ================================================ import { expect } from 'chai'; import { configLocate } from '../file-locator'; describe('locate', () => { const packageDirPath = 'packages/hadron-file-locator/src/__tests__'; it('should return an array', () => { return configLocate(['./mock/app/config/*'], 'config', 'development').then( (result) => { expect(result).to.be.an('array'); }, ); }); it('should return path files from mock/app/conifg/* with development type and js extension', () => { return configLocate( [`${packageDirPath}/mock/app/config/*`], 'config', 'development', ['JS'], ).then((result) => { expect(result).to.deep.equal([ `${packageDirPath}/mock/app/config/config.js`, `${packageDirPath}/mock/app/config/config_development.js`, ]); }); }); it('should return path files from mock/app/config/* with development type and json extension', () => { return configLocate( [`${packageDirPath}/mock/app/config/*`], 'config', 'development', ['json'], ).then((result) => { expect(result).to.deep.equal([ `${packageDirPath}/mock/app/config/config.json`, `${packageDirPath}/mock/app/config/config_development.json`, ]); }); }); it('should return path files from every folder in mock/plugins with config folder', () => { return configLocate( [`${packageDirPath}/mock/plugins/*/config/*`], 'config', 'development', ['js'], ).then((result) => { expect(result).to.deep.equal([ `${packageDirPath}/mock/plugins/plugin1/config/config.js`, `${packageDirPath}/mock/plugins/plugin1/config/config_development.js`, `${packageDirPath}/mock/plugins/plugin2/config/config.js`, `${packageDirPath}/mock/plugins/plugin2/config/config_development.js`, `${packageDirPath}/mock/plugins/plugin3/config/config.js`, `${packageDirPath}/mock/plugins/plugin3/config/config_development.js`, ]); }); }); it('should return path files from mock/app/ext with js, json and xml extensions', () => { return configLocate( [`${packageDirPath}/mock/app/ext/*`], 'config', 'development', ['js', 'json', 'xml'], ).then((result) => { expect(result).to.deep.equal([ `${packageDirPath}/mock/app/ext/config.js`, `${packageDirPath}/mock/app/ext/config.json`, `${packageDirPath}/mock/app/ext/config.xml`, ]); }); }); }); ================================================ FILE: packages/hadron-file-locator/src/__tests__/mock/app/config/config.js ================================================ const x = () => { return { usernameJS: "user-JS", emailJS: "user-JS@email.com", } } module.exports = x; ================================================ FILE: packages/hadron-file-locator/src/__tests__/mock/app/config/config.json ================================================ { "status": "Default", "database": { "host": "default", "user": "default", "password": "default" } } ================================================ FILE: packages/hadron-file-locator/src/__tests__/mock/app/config/config.xml ================================================ Test ================================================ FILE: packages/hadron-file-locator/src/__tests__/mock/app/config/config_development.js ================================================ const x = () => { return { name: "module - x" } } module.exports = x; ================================================ FILE: packages/hadron-file-locator/src/__tests__/mock/app/config/config_development.json ================================================ { "status": "Development", "onlyDevelopmentProp": "I am developer!" } ================================================ FILE: packages/hadron-file-locator/src/__tests__/mock/app/config/config_test.js ================================================ ================================================ FILE: packages/hadron-file-locator/src/__tests__/mock/app/ext/config.js ================================================ ================================================ FILE: packages/hadron-file-locator/src/__tests__/mock/app/ext/config.json ================================================ ================================================ FILE: packages/hadron-file-locator/src/__tests__/mock/app/ext/config.xml ================================================ ================================================ FILE: packages/hadron-file-locator/src/__tests__/mock/app/universal/dog.config.js ================================================ const x = () => { return { dogName: "Rex", } } module.exports = x; ================================================ FILE: packages/hadron-file-locator/src/__tests__/mock/app/universal/other.json ================================================ { "fromJSON": true } ================================================ FILE: packages/hadron-file-locator/src/__tests__/mock/app/universal/team.config.js ================================================ const x = () => { return { teamName: "team1", } } module.exports = x; ================================================ FILE: packages/hadron-file-locator/src/__tests__/mock/app/universal/test.js ================================================ const x = () => { return { name: "TEST", } } module.exports = x; ================================================ FILE: packages/hadron-file-locator/src/__tests__/mock/app/universal/user.config.js ================================================ const x = () => { return { userName: "user1", } } module.exports = x; ================================================ FILE: packages/hadron-file-locator/src/__tests__/mock/plugins/plugin1/config/config.js ================================================ ================================================ FILE: packages/hadron-file-locator/src/__tests__/mock/plugins/plugin1/config/config_development.js ================================================ ================================================ FILE: packages/hadron-file-locator/src/__tests__/mock/plugins/plugin1/config/config_development.ts ================================================ ================================================ FILE: packages/hadron-file-locator/src/__tests__/mock/plugins/plugin1/config/config_test.js ================================================ ================================================ FILE: packages/hadron-file-locator/src/__tests__/mock/plugins/plugin2/config/config.js ================================================ ================================================ FILE: packages/hadron-file-locator/src/__tests__/mock/plugins/plugin2/config/config_development.js ================================================ ================================================ FILE: packages/hadron-file-locator/src/__tests__/mock/plugins/plugin2/config/config_test.js ================================================ ================================================ FILE: packages/hadron-file-locator/src/__tests__/mock/plugins/plugin3/config/config.js ================================================ ================================================ FILE: packages/hadron-file-locator/src/__tests__/mock/plugins/plugin3/config/config_development.js ================================================ ================================================ FILE: packages/hadron-file-locator/src/__tests__/mock/plugins/plugin3/config/config_test.js ================================================ ================================================ FILE: packages/hadron-file-locator/src/declarations.d.ts ================================================ declare module 'glob'; ================================================ FILE: packages/hadron-file-locator/src/file-locator.ts ================================================ import glob from './glob-promise'; const addExtension = (paths: string[], extensions: string[]): string[] => paths.reduce( (accumulator, currentPath) => [ ...accumulator, ...extensions.map((ext) => `${currentPath}.${ext.toLowerCase()}`), ], [], ); const filterPaths = (data: string[], configName: string, type: string) => new Promise((resolve, reject) => { const arr: string[] = []; data.forEach((element) => { const fileName = element .split('/') [element.split('/').length - 1].split('.')[0]; if (fileName === configName) { arr.push(element); } else if (fileName.includes('_')) { if ( fileName.split('_')[0] === configName && fileName.split('_')[1] === type ) { arr.push(element); } } }); resolve(arr); }); const flattenDeep = (arr: any[]): any[] => Array.isArray(arr) ? arr.reduce((a, b) => [...flattenDeep(a), ...flattenDeep(b)], []) : [arr]; export const configLocate = ( paths: string[], configName: string, type: string, extensions: string[] = [], ): Promise => new Promise((resolve, reject) => { paths.map((path) => path.toLowerCase()); if (extensions.length > 0) { paths = addExtension(paths, extensions); } Promise.all(paths.map(glob)).then((data) => { filterPaths( flattenDeep(data.map((el) => el.sort())), configName, type, ).then(resolve); }); }); export const locate = ( paths: string[], extensions: string[] = [], ): Promise => new Promise((resolve, reject) => { paths.map((path) => path.toLowerCase()); if (extensions.length > 0) { paths = addExtension(paths, extensions); } Promise.all(paths.map(glob)) .then((data) => flattenDeep(data.map((el) => el.sort()))) .then(resolve); }); export default locate; ================================================ FILE: packages/hadron-file-locator/src/glob-promise.ts ================================================ import * as glob from 'glob'; const promise = (pattern: string): Promise => new Promise( (resolve, reject) => new glob.Glob( pattern, (err: Error, data: string[]) => err === null ? resolve(data) : reject(err), ), ); export default promise; ================================================ FILE: packages/hadron-file-locator/tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "target": "es6", "noImplicitAny": true, "noEmitOnError": true, "moduleResolution": "node", "sourceMap": true, "outDir": "dist", "baseUrl": ".", "paths": { "*": ["./*"] }, "emitDecoratorMetadata": true, "experimentalDecorators": true, "inlineSources": true, "lib": ["es2017", "dom"] }, "include": ["src/**/*.*", "index.ts", "LICENSE"], "exclude": ["node_modules", "**/__tests__/**", "**/*/*.d.ts"] } ================================================ FILE: packages/hadron-json-provider/LICENSE ================================================ MIT License Copyright (c) 2018 Brainhub Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/hadron-json-provider/README.md ================================================ ## Installation ```bash npm install @brainhubeu/hadron-json-provider --save ``` [More info about installation](/core/#installation) ## Overview JSON Provider allows you to automatically load multiple files as JSON object, with file names as object keys, and files data as object values. Currently we support following extensions: * `.js` * `.json` * `.xml` ## Module functions ### Basic provider ```javascript jsonProvider(paths, extensions); ``` * `paths` - array of strings which contains paths to files * `extensions` - array of strings which contains extensions of files from which you want to build an JSON object For example, having directory with the following structure: ![Directory structure](img/routing.png) To find all files in `./routing` and its sub-directories with extension `config.js` you can use following code: ```javascript jsonProvider(['./routing/**/*'], ['config.js']) .then((object) => {}) .catch((error) => {}); ``` ### Configuration Provider ```javascript configJsonProvider(paths, configFile, projectType, extensions); ``` * `paths` - array of strings which contains paths to files * `configFile` - name of main configuration file * `projectType` - project type * `extensions` - array of strings which contains extensions of files from which you want to build an JSON object For example, having directory with the following structure: ![Directory structure](img/routingType.png) If you want to build configuration object which depends on project type, for example `development` you can use following code ```javascript configJsonProvider(['./app/config/*'], 'config', 'development', ['xml', 'js']) .then((object) => {}) .catch((error) => {}); ``` ================================================ FILE: packages/hadron-json-provider/index.ts ================================================ import jsonProvider, { configJsonProvider, jsLoader, jsonLoader, xmlLoader, } from './src/json-provider'; export default jsonProvider; export { configJsonProvider, jsLoader, jsonLoader, xmlLoader }; ================================================ FILE: packages/hadron-json-provider/package.json ================================================ { "name": "@brainhubeu/hadron-json-provider", "version": "1.0.0", "description": "Hadron json provider module", "main": "dist/index.js", "files": [ "dist", "./LICENSE" ], "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "prepare": "tsc" }, "keywords": [ "hadron", "brainhub", "hadron-json-provider" ], "author": "Brainhub", "license": "MIT", "dependencies": { "@brainhubeu/hadron-file-locator": "^1.0.0", "xml2js": "0.4.19" }, "devDependencies": { "@types/xml2js": "0.4.2" }, "publishConfig": { "access": "public" } } ================================================ FILE: packages/hadron-json-provider/src/__tests__/config-json-provider.ts ================================================ import { expect } from 'chai'; import { configJsonProvider } from '../json-provider'; describe('configJsonProvider', () => { const mockDirPath = 'packages/hadron-json-provider/src/__tests__'; it('should return object', () => { return configJsonProvider( [`${mockDirPath}/mock/app/config/*`], 'config', 'development', ['js'], ).then((result) => { expect(result).to.be.an.instanceof(Object); }); }); it('should return JavaScript object with proper values', () => { const validObject = { emailJS: 'user-JS@email.com', name: 'module - x', usernameJS: 'user-JS', }; return configJsonProvider( [`${mockDirPath}/mock/app/config/*`], 'config', 'development', ['js'], ).then((result) => { expect(result).to.be.deep.equal(validObject); }); }); it('should return JavaScript object from json and xml files', () => { const validObject = { database: { host: 'default', password: 'default', user: 'default', }, status: 'Test', }; return configJsonProvider( [`${mockDirPath}/mock/app/config/*`], 'config', 'development', ['json', 'xml'], ).then((result) => { expect(result).to.be.deep.equal(validObject); }); }); it('should return JavaScript object from json and js files', () => { const validObject = { database: { host: 'default', password: 'default', user: 'default', }, emailJS: 'user-JS@email.com', name: 'module - x', status: 'Development', usernameJS: 'user-JS', }; return configJsonProvider( [`${mockDirPath}/mock/app/config/*`], 'config', 'development', ['json', 'js'], ).then((result) => { expect(result).to.be.deep.equal(validObject); }); }); }); ================================================ FILE: packages/hadron-json-provider/src/__tests__/js-loader.ts ================================================ import { expect } from 'chai'; import { jsLoader } from '../json-provider'; describe('jsLoader', () => { const mockDirPath = 'packages/hadron-json-provider/src/__tests__'; it('should return object', () => { return jsLoader(`${mockDirPath}/mock/app/config/config.js`).then( (result) => { expect(result).to.be.an.instanceof(Object); }, ); }); it('load JavaScript file and return callback result', () => { return jsLoader(`${mockDirPath}/mock/app/config/config.js`).then( (result) => { expect(result).to.be.deep.equal({ emailJS: 'user-JS@email.com', usernameJS: 'user-JS', }); }, ); }); it('should load a JavaScript file with default export', () => { return jsLoader( `${mockDirPath}/mock/app/universal/exportDefaultConfig.js`, ).then((result) => { expect(result).to.be.deep.equal({ emailJS: 'user-JS@email.com', usernameJS: 'user-JS', }); }); }); it("should throw error if file path doesn't have a valid extension", () => { const path = `${mockDirPath}/mock/app/config/config.json`; return jsLoader(path).catch((error) => { expect(error).to.be.an.instanceof(Error); expect(error.message).to.be.equal(`${path} doesn't have js extension`); }); }); }); ================================================ FILE: packages/hadron-json-provider/src/__tests__/json-loader.ts ================================================ import { expect } from 'chai'; import { jsonLoader } from '../json-provider'; describe('jsonLoader', () => { const path = 'packages/hadron-json-provider/src/__tests__/mock/app/config/config_development.json'; it('should return an object', () => { return jsonLoader(path).then((result) => { expect(result).to.be.an.instanceof(Object); }); }); it('load json file and return callback result', () => { return jsonLoader(path).then((result) => { expect(result).to.be.deep.equal({ status: 'Development', }); }); }); it('should throw an error when extension is invalid', () => { return jsonLoader(`${path}x`).catch((error) => { expect(error).to.be.an.instanceof(Error); expect(error.message).to.be.equal( `${path}x doesn't have a json extension`, ); }); }); }); ================================================ FILE: packages/hadron-json-provider/src/__tests__/json-provider.ts ================================================ import { expect } from 'chai'; import jsonProvider from '../json-provider'; describe('jsonProvider', () => { const mockDirPath = 'packages/hadron-json-provider/src/__tests__'; it("should return object from config files, supports custom paths (like 'config.js')", () => { const finalObject = { dogName: 'Rex', teamName: 'team1', userName: 'user1', }; return jsonProvider( [`${mockDirPath}/mock/app/universal/*`], ['config.js'], ).then((object) => { expect(object).to.be.deep.equal(finalObject); }); }); it('should return empty object if files do not exists', () => { return jsonProvider(['a/b/c'], ['js']).then((data) => expect(data).to.be.deep.equal({}), ); }); it('should combine configurations from different extensions', () => { const finalObject = { dogName: 'Rex', fromJSON: true, teamName: 'team1', userName: 'user1', }; return jsonProvider( [`${mockDirPath}/mock/app/universal/*`], ['config.js', 'json'], ).then((object) => { expect(object).to.be.deep.equal(finalObject); }); }); }); ================================================ FILE: packages/hadron-json-provider/src/__tests__/mock/app/config/config.js ================================================ const x = () => { return { usernameJS: "user-JS", emailJS: "user-JS@email.com", } } module.exports = x; ================================================ FILE: packages/hadron-json-provider/src/__tests__/mock/app/config/config.json ================================================ { "status": "Default", "database": { "host": "default", "user": "default", "password": "default" } } ================================================ FILE: packages/hadron-json-provider/src/__tests__/mock/app/config/config.xml ================================================ Test ================================================ FILE: packages/hadron-json-provider/src/__tests__/mock/app/config/config_development.js ================================================ const x = () => { return { name: "module - x" } } module.exports = x; ================================================ FILE: packages/hadron-json-provider/src/__tests__/mock/app/config/config_development.json ================================================ { "status": "Development" } ================================================ FILE: packages/hadron-json-provider/src/__tests__/mock/app/config/config_test.js ================================================ ================================================ FILE: packages/hadron-json-provider/src/__tests__/mock/app/ext/config.js ================================================ ================================================ FILE: packages/hadron-json-provider/src/__tests__/mock/app/ext/config.json ================================================ ================================================ FILE: packages/hadron-json-provider/src/__tests__/mock/app/ext/config.xml ================================================ ================================================ FILE: packages/hadron-json-provider/src/__tests__/mock/app/universal/dog.config.js ================================================ const x = () => { return { dogName: "Rex", } } module.exports = x; ================================================ FILE: packages/hadron-json-provider/src/__tests__/mock/app/universal/exportDefaultConfig.js ================================================ const x = () => { return { usernameJS: 'user-JS', emailJS: 'user-JS@email.com', }; }; module.exports.default = x; ================================================ FILE: packages/hadron-json-provider/src/__tests__/mock/app/universal/other.json ================================================ { "fromJSON": true } ================================================ FILE: packages/hadron-json-provider/src/__tests__/mock/app/universal/team.config.js ================================================ const x = () => { return { teamName: "team1", } } module.exports = x; ================================================ FILE: packages/hadron-json-provider/src/__tests__/mock/app/universal/test.js ================================================ const x = () => { return { name: "TEST", } } module.exports = x; ================================================ FILE: packages/hadron-json-provider/src/__tests__/mock/app/universal/user.config.js ================================================ const x = () => { return { userName: "user1", } } module.exports = x; ================================================ FILE: packages/hadron-json-provider/src/__tests__/mock/plugins/plugin1/config/config.js ================================================ ================================================ FILE: packages/hadron-json-provider/src/__tests__/mock/plugins/plugin1/config/config_development.js ================================================ ================================================ FILE: packages/hadron-json-provider/src/__tests__/mock/plugins/plugin1/config/config_development.ts ================================================ ================================================ FILE: packages/hadron-json-provider/src/__tests__/mock/plugins/plugin1/config/config_test.js ================================================ ================================================ FILE: packages/hadron-json-provider/src/__tests__/mock/plugins/plugin2/config/config.js ================================================ ================================================ FILE: packages/hadron-json-provider/src/__tests__/mock/plugins/plugin2/config/config_development.js ================================================ ================================================ FILE: packages/hadron-json-provider/src/__tests__/mock/plugins/plugin2/config/config_test.js ================================================ ================================================ FILE: packages/hadron-json-provider/src/__tests__/mock/plugins/plugin3/config/config.js ================================================ ================================================ FILE: packages/hadron-json-provider/src/__tests__/mock/plugins/plugin3/config/config_development.js ================================================ ================================================ FILE: packages/hadron-json-provider/src/__tests__/mock/plugins/plugin3/config/config_test.js ================================================ ================================================ FILE: packages/hadron-json-provider/src/__tests__/xml-loader.ts ================================================ import { expect } from 'chai'; import { xmlLoader } from '../json-provider'; describe('xmlLoader', () => { const path = 'packages/hadron-json-provider/src/__tests__/mock/app/config/config.xml'; it('should return object', () => { return xmlLoader(path).then((result) => { expect(result).to.be.an.instanceof(Object); }); }); it('load XML file and return callback result', () => { return xmlLoader(path).then((result) => { expect(result).to.be.deep.equal({ status: 'Test', }); }); }); it("should throw error if file path doesn't have a valid extension", () => { return xmlLoader(`${path}x`).catch((error) => { expect(error).to.be.an.instanceof(Error); expect(error.message).to.be.equal(`${path}x doesn't have xml extension`); }); }); }); ================================================ FILE: packages/hadron-json-provider/src/declarations.d.ts ================================================ declare module 'xml2js'; declare module '@hadron/file-locator'; ================================================ FILE: packages/hadron-json-provider/src/json-provider.ts ================================================ import * as fs from 'fs'; import { extname, relative } from 'path'; import { parseString as xmlToJson } from 'xml2js'; import locate, { configLocate } from '@brainhubeu/hadron-file-locator'; const isFunction = (functionToCheck: any): boolean => functionToCheck && {}.toString.call(functionToCheck) === '[object Function]'; const getExtension = (path: string): string => extname(path).substring(1); export const jsLoader = (path: string) => { const supportsExtension: string = 'js'; return new Promise((resolve, reject) => { if (getExtension(path) !== supportsExtension) { reject(new Error(`${path} doesn't have ${supportsExtension} extension`)); } let data = require(`./${relative(__dirname, path)}`); if (!isFunction(data) && data && isFunction(data.default)) { data = data.default; } data !== null ? resolve(data()) : reject(new Error('File not found')); }); }; export const jsonLoader = (path: string) => { const supportsExtension: string = 'json'; return new Promise((resolve, reject) => { if (getExtension(path) !== supportsExtension) { reject( new Error(`${path} doesn't have a ${supportsExtension} extension`), ); } fs.readFile(path, 'utf8', (err, data) => { err ? reject(err) : resolve(JSON.parse(data)); }); }); }; export const xmlLoader = (path: string) => { const supportsExtension: string = 'xml'; return new Promise((resolve, reject) => { if (getExtension(path) !== supportsExtension) { reject(new Error(`${path} doesn't have ${supportsExtension} extension`)); } fs.readFile(path, 'utf8', (err, data) => { if (err) { reject(err); } xmlToJson(data, (jsonErr: Error, jsonData: string) => { if (jsonErr) { reject(jsonErr); } resolve(jsonData); }); }); }); }; const mapper: { [key: string]: (path: string) => Promise } = { js: jsLoader, json: jsonLoader, xml: xmlLoader, }; const extensionMapper = (paths: string[]): Array> => paths.map((path) => mapper[getExtension(path)](path)); export const configJsonProvider = ( paths: string[], configName: string, type: string, extensions: string[] = [], concatResults: boolean = false, ): Promise => configLocate(paths, configName, type, extensions) .then((locatedPaths: string[]) => Promise.all(extensionMapper(locatedPaths)), ) .then( (data: any) => (concatResults ? [...data] : Object.assign({}, ...data)), ); export const jsonProvider = ( paths: string[], extensions: string[], concatResults: boolean = false, ) => locate(paths, extensions) .then((locatedPaths: string[]) => Promise.all(extensionMapper(locatedPaths)), ) .then( (data: any) => (concatResults ? [...data] : Object.assign({}, ...data)), ); export default jsonProvider; ================================================ FILE: packages/hadron-json-provider/tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "target": "es6", "noImplicitAny": true, "noEmitOnError": true, "moduleResolution": "node", "sourceMap": true, "outDir": "dist", "baseUrl": ".", "paths": { "*": ["./*"] }, "emitDecoratorMetadata": true, "experimentalDecorators": true, "inlineSources": true, "lib": ["es2017", "dom"] }, "include": ["src/**/*.*", "index.ts", "LICENSE"], "exclude": ["node_modules", "**/__tests__/**", "**/*/*.d.ts"] } ================================================ FILE: packages/hadron-logger/LICENSE ================================================ MIT License Copyright (c) 2018 Brainhub Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/hadron-logger/README.md ================================================ # Logger for Hadron ## Overview Hadron Logger provides an option to replace the default hadron logger ([bunyan](https://github.com/trentm/node-bunyan)) to the one of your choice. ## Installation ```bash npm install @brainhubeu/hadron-logger --save ``` [More info about installation](http://hadron-docs.dev.brainhub.pl/core/#installation) ## Initialization Pass the package as an argument for the Hadron bootstrapping function: ```javascript // ... importing and initializing other components hadron(expressApp, [require('@brainhubeu/hadron-logger')], config); ``` That way, you should be able to get it from the [Container](http://hadron-docs.dev.brainhub.pl/core/#dependency-injection) like that: ```javascript const logger = container.take('logger'); logger.log('Hello, I am your logger'); logger.warm('Look out! I am your logger!'); logger.debug('Am I your logger?'); logger.error('I am not your logger!'); ``` Notice: `logger` is a container key only for the default logger. ## Configuration To setup your own logger, you need to provide an adapter first. You can do that by importing the `registerAdapter` method and calling it with name and provider function for your logger, like that: ```javascript const registerAdapter = require('@brainhubeu/hadron-logger').registerAdapter; registerAdapter('myOwnLogger', function(config) { return { log: function(message) { console.log(message); }, warn: function(message) { console.warn(message); }, debug: function(message) { console.debug(message); }, error: function(message) { console.error(message); }, }; }); ``` Provider takes the Hadron logger config as the first parameter. After your adapter is set up, you can define your logger in the Hadron configuration and retrieve it using the logger's name. ```javascript const hadronConfig = { // ...other stuff logger: { name: 'myLoggerName', type: 'myOwnLogger', }, }; // hadron initialization const logger = Container.take('myLoggerName'); ``` ## Multiple loggers You can define multiple loggers for your application. To do that you just need to provide adapters for each of them and define them in the configuration. ```javascript const hadronConfig = { // ...other stuff logger: [ { name: 'Logger1', type: 'logger1' }, { name: 'logger2', type: 'logger2' }, { name: 'logger3', type: 'logger3' }, ], }; // hadron initialization container.take('Logger1'); container.take('Logger2'); container.take('Logger3'); ``` ================================================ FILE: packages/hadron-logger/index.ts ================================================ import logger, { register, registerAdapter } from './src/logger'; import { ILogger, ILoggerConfig, ILoggerFactory } from './src/types'; export default logger; export { register, ILogger, ILoggerConfig, ILoggerFactory, registerAdapter }; ================================================ FILE: packages/hadron-logger/package.json ================================================ { "name": "@brainhubeu/hadron-logger", "version": "1.0.1", "description": "Hadron logger", "target": "dist", "main": "dist/index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "prepare": "tsc" }, "keywords": [ "hadron", "logger" ], "author": "Brainhub", "license": "MIT", "dependencies": { "@brainhubeu/hadron-core": "^1.0.0", "@brainhubeu/hadron-error-handler": "^1.0.0", "bunyan": "^1.8.12" }, "devDependencies": { "@types/bunyan": "^1.8.4" }, "publishConfig": { "access": "public" } } ================================================ FILE: packages/hadron-logger/src/__tests__/logger.ts ================================================ import { expect, assert } from 'chai'; import { Container } from '@brainhubeu/hadron-core'; import { register, registerAdapter } from '../logger'; import LoggerNameIsRequiredError from '../errors/LoggerNameIsRequiredError'; import LoggerAdapterNotDefinedError from '../errors/LoggerAdapterNotDefinedError'; import CouldNotRegisterLoggerInContainerError from '../errors/CouldNotRegisterLoggerInContainerError'; import ConfigNotDefinedError from '../errors/ConfigNotDefinedError'; import { ILogger } from '../types'; describe('logger', () => { beforeEach(() => { Container.register('firstlogger', ''); Container.register('firstLogger', ''); Container.register('secondLogger', ''); Container.register('UnknownTypeLogger', ''); }); it('should register logger under "firstlogger"', () => { register(Container, { logger: { name: 'firstlogger', type: 'bunyan', }, }); assert(Container.take('firstlogger')); }); it('should register multiple loggers', () => { register(Container, { logger: [ { name: 'firstLogger', type: 'bunyan', }, { name: 'secondLogger', type: 'bunyan', }, ], }); assert(Container.take('firstLogger')); assert(Container.take('secondLogger')); }); it('when there is no name provided in config then should throw LoggerNameIsRequiredError', () => { expect(() => { register(Container, { logger: { type: 'bunyan', }, } as any); }).to.throw(LoggerNameIsRequiredError); }); it('when there is no type of logger, should register it with default logger', () => { register(Container, { logger: { name: 'firstLogger', }, } as any); assert(Container.take('firstLogger')); }); it('when logger is defined with type that has not any adapter then should throw LoggerAdapterNotDefinedError', () => { expect(() => { register(Container, { logger: { type: 'unknown-type', name: 'UnknownTypeLogger', }, }); }).to.throw(LoggerAdapterNotDefinedError); }); it('should register logger with custom adapter', () => { registerAdapter('custom', (config: any) => ({})); register(Container, { logger: { name: 'firstLogger', type: 'custom', }, }); assert(Container.take('firstLogger')); }); it('when custom adapter is broken should throw CouldNotRegisterLoggerInContainerError', () => { registerAdapter('custom', null); expect(() => { register(Container, { logger: { name: 'firstLogger', type: 'custom', }, }); }).to.throw(CouldNotRegisterLoggerInContainerError); }); it('when did not provided logger info then should throw ConfigNotDefinedError', () => { expect(() => { register(Container, {} as any); }).to.throw(ConfigNotDefinedError); }); }); ================================================ FILE: packages/hadron-logger/src/adapters/__tests__/bunyan.ts ================================================ import { assert } from 'chai'; import * as sinon from 'sinon'; import * as bunyan from 'bunyan'; import { ILogger } from '../../types'; import bunyanAdapter from '../bunyan'; describe('logger-adapter: bunyan', () => { let logger: ILogger; const spies: any = { info: sinon.spy(), debug: sinon.spy(), warn: sinon.spy(), error: sinon.spy(), }; const createLoggerStub = sinon.stub(bunyan, 'createLogger'); createLoggerStub.returns(spies); after(() => { createLoggerStub.restore(); }); afterEach(() => { Object.keys(spies).forEach((spy: string) => { spies[spy].reset(); }); }); it('should create logger instance', () => { logger = bunyanAdapter({ name: 'test', }); assert('log' in logger); }); it('should execute info method', () => { logger.log('test'); assert(spies.info.called); }); it('should execute debug method', () => { logger.debug('test'); assert(spies.debug.called); }); it('should execute warn method', () => { logger.warn('test'); assert(spies.warn.called); }); it('should execute error method', () => { logger.error('test'); assert(spies.error.called); }); }); ================================================ FILE: packages/hadron-logger/src/adapters/bunyan.ts ================================================ import * as bunyan from 'bunyan'; import { ILogger, ILoggerConfig } from '../types'; export default (config: ILoggerConfig): ILogger => { const logger: bunyan = bunyan.createLogger(config); return { log: (message: string) => { logger.info(message); }, debug: (message: string) => { logger.debug(message); }, warn: (message: string) => { logger.warn(message); }, error: (message: string) => { logger.error(message); }, }; }; ================================================ FILE: packages/hadron-logger/src/adapters/index.ts ================================================ import bunyan from '../adapters/bunyan'; import { ILogger, ILoggerFactory } from '../types'; const adapters: { [name: string]: ILoggerFactory } = { bunyan, }; export default adapters; ================================================ FILE: packages/hadron-logger/src/errors/ConfigNotDefinedError.ts ================================================ import HadronErrorHandler from '@brainhubeu/hadron-error-handler'; export default class ConfigNotDefinedError extends HadronErrorHandler { constructor(error: Error = new Error()) { super(`Config for hadron-logger package has not been found`); this.error = error; this.stack = error.stack; } } ================================================ FILE: packages/hadron-logger/src/errors/CouldNotRegisterLoggerInContainerError.ts ================================================ import HadronErrorHandler from '@brainhubeu/hadron-error-handler'; export default class CouldNotRegisterLoggerInContainerError extends HadronErrorHandler { constructor(name: string, error: Error = new Error()) { super(`Could not create and register logger '${name}' in container.`); this.error = error; this.stack = error.stack; } } ================================================ FILE: packages/hadron-logger/src/errors/LoggerAdapterNotDefinedError.ts ================================================ import HadronErrorHandler from '@brainhubeu/hadron-error-handler'; export default class LoggerAdapterNotDefinedError extends HadronErrorHandler { constructor(name: string, error: Error = new Error()) { super(`Logger adapter for ${name} has not been not found.`); this.error = error; this.stack = error.stack; } } ================================================ FILE: packages/hadron-logger/src/errors/LoggerNameIsRequiredError.ts ================================================ import HadronErrorHandler from '@brainhubeu/hadron-error-handler'; export default class LoggerNameIsRequiredError extends HadronErrorHandler { constructor(error: Error = new Error()) { super(`logger name is required to register in hadron`); this.error = error; this.stack = error.stack; } } ================================================ FILE: packages/hadron-logger/src/logger.ts ================================================ import { IContainer } from '@brainhubeu/hadron-core'; import LoggerNameIsRequiredError from './errors/LoggerNameIsRequiredError'; import LoggerAdapterNotDefinedError from './errors/LoggerAdapterNotDefinedError'; import ConfigNotDefinedError from './errors/ConfigNotDefinedError'; import CouldNotRegisterLoggerInContainerError from './errors/CouldNotRegisterLoggerInContainerError'; import defaultAdapters from './adapters'; import { ILogger, ILoggerConfig, ILoggerFactory, IHadronLoggerConfig, } from './types'; const adapters: { [key: string]: ILoggerFactory } = { ...defaultAdapters }; export const registerAdapter = ( name: string, adapter: ILoggerFactory, ): void => { adapters[name] = adapter; }; const register = (container: IContainer, config: IHadronLoggerConfig) => { let { logger: loggers } = config; if (!loggers) { throw new ConfigNotDefinedError(); } loggers = loggers instanceof Array ? loggers : [loggers]; loggers.forEach((logger: ILoggerConfig) => { const { type = 'bunyan', ...loggerConfig } = logger; if (!(type in adapters)) { throw new LoggerAdapterNotDefinedError(type); } if (!logger.name) { throw new LoggerNameIsRequiredError(); } try { const loggerInstance: ILogger = adapters[type](loggerConfig); container.register(logger.name, loggerInstance); } catch (error) { throw new CouldNotRegisterLoggerInContainerError(logger.name); } }); }; export { register }; export default register; ================================================ FILE: packages/hadron-logger/src/types.ts ================================================ export interface IHadronLoggerConfig { logger: ILoggerConfig[] | ILoggerConfig; } export interface ILoggerConfig { type?: string; name: string; } export interface ILogger { log?: (message: string) => void; warn?: (message: string) => void; debug?: (message: string) => void; error?: (message: string) => void; } export type ILoggerFactory = (config: ILoggerConfig) => ILogger; ================================================ FILE: packages/hadron-logger/tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "target": "es6", "noImplicitAny": true, "noEmitOnError": true, "moduleResolution": "node", "sourceMap": true, "outDir": "dist", "baseUrl": ".", "paths": { "*": ["./*"] }, "emitDecoratorMetadata": true, "experimentalDecorators": true, "inlineSources": true, "lib": ["es2017", "dom"] }, "include": ["src/**/*.*", "index.ts", "LICENSE"], "exclude": ["node_modules", "**/__tests__/**", "**/*/*.d.ts"] } ================================================ FILE: packages/hadron-oauth/.eslintrc ================================================ { extends: 'brainhub' } ================================================ FILE: packages/hadron-oauth/.gitignore ================================================ *.swp ================================================ FILE: packages/hadron-oauth/LICENSE ================================================ MIT License Copyright (c) 2018 Brainhub Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/hadron-oauth/README.md ================================================ ## Installation `npm install --save @brainhubeu/hadron-oauth` ## Overview `hadron-oauth` is a Hadron utility package to simplify working with OAuth providers, such as Google and Facebook. It provides several utility functions that you can use to make writing OAuth2 authentication quicker and more streamlined. ## Tutorial ### Understanding OAuth flow The plan for our authentication flow is as follows: * The client makes a `GET` request to `/auth/{provider}` to begin the process of authentication. * The server redirects the client to a provider consent site. * The client receives an auth code. * The client makes a `POST` request to `/auth/{provider}/token` to exchange the auth code for an access token. We can then use the access token to make further request to the provider's API in order to fetch data about the user (such as their name or email). ### Configuration We will need to provide certain information to `hadron-oauth` before we can proceed with the auth process. This information exists in Hadron's config file. ```js // oauth.js module.exports = { google: { clientID: 'keyboard-cat', clientSecret: 'shhh', scope: ['https://www.googleapis.com/auth/userinfo.profile'], redirectUri: 'http://localhost:8081/', }, }; ``` The `clientID` and `clientSecret` fields need to be the same as these given to you by your provider (such as the Google API Console). The `redirectUri` must be exactly the same as the one given to your provider. `scope` is an array of strings defining the scopes your app requires from the user. See the [Google scopes](https://developers.google.com/identity/protocols/googlescopes) and [Facebook scopes](https://developers.facebook.com/docs/facebook-login/permissions/) for details. These can be used later to retrieve relevant data from your provider's APIs about the user. In this case, we'll also pretend that there is a front-end dev server at `http://localhost:8081`, however we can just as well redirect the calls back to our own server. It's recommended that you exclude this file from your version control or supply the specific config fields through environment variables as this file contains sensitive information. ### Registration Now that our config is created, we need to create our Hadron app entry point. ```js // index.js const hadron = require('@brainhubeu/hadron-core').default; const hadronExpress = require('@brainhubeu/hadron-express'); const hadronOauth = require('@brainhubeu/hadron-oauth'); const express = require('express'); const oauthConfig = require('./oauth.js'); const app = express(); const config = { oauth: oauthConfig, routes: { root: { path: '/', methods: ['GET'], callback: () => 'Hello!', }, }, }; hadron(app, [hadronExpress, hadronOauth], config).then(() => { app.listen(8080, () => { console.log('Hadron/Express listening on 8080.'); }); }); ``` We now have access to the OAuth methods through the container. ### Auth code route Let's create a separate file to store the logic for our route endpoints. We'll first create a route to redirect to the consent screen. ```js // routes.js const routes = { googleAuthRequest: { path: '/auth/google', methods: ['GET'], callback: (req, { oauth }) => ({ redirect: oauth.google.redirect(), }), }, }; module.exports = routes; ``` ```js // index.js const oauthRoutes = require('./routes'); // ... const config = { routes: { root: { // ... }, ...oauthRoutes, }, }; ``` Now, whenever a client calls the `/auth/google` endpoint, he or she will be redirected to the Google auth consent page. ### Handling the auth code Now, we'll delve into the client side here for a minute, because we need to send the auth code that the client receives back to the server. In the `oauth.js` config file we defined a redirect to our front-end dev server. We now need to send the authorization code from that server back to our app. We can do it like this: ```js // client side javascript const url = new URL(window.location); const params = new URLSearchParams(url.search); const code = params.get('code'); if (!code) return; fetch('http://localhost:8080/auth/google/token', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ code }), }).then((res) => { // ... }); ``` ### Exchanging the code for an access token. We'll define another route to exchange the auth code for an access token. ```js // routes.js const routes = { googleAuthRequest: { // ... }, googleTokenRequest: { path: '/auth/google/token', methods: ['POST'], callback: (req, { oauth }) => { oauth.google.token(req.body.code).then((res) => { console.log(res.access_token); // do things with the token... }); }, }, }; module.exports = routes; ``` Now that we have the access token we can implement other features, such as our own authentication. We can also call the Google API in the name of the user to pull any relevant information we need. ## Reference ### Config #### `google.` * `clientID: string` - your app id as provided by the [Google API Console](https://console.cloud.google.com/apis/credentials). * `clientSecret: string` - your app secret from the Google API Console * `scope: string[]` - an array of strings listing the [scope URLs](https://developers.google.com/identity/protocols/googlescopes) you need for your app. * `redirectUri: string` - redirect URI for your app, must be exactly the same as the one you chose in Google API Console. * `authUrl: ?string` - an optional parameter to provide a different auth URL to Google (for instance if the current one was to stop working). Defaults to `https://accounts.google.com/o/oauth2/v2/auth`. * `tokenUrl: ?string` - an optional parameter to provide a different token exchange Google API endpoint. Defaults to `https://www.googleapis.com/oauth2/v4/token`. * `responseType: ?string` - if Google was to support a different kind of OAuth authentication flow, we could specify the response type here. Currently, it only supports `code` and so it defaults to that. * `grantType: ?string` - similarly to the point above, if Google was to support a different kind of OAuth authentication flow, we could specify the grant type here. Defaults to `authorization_code`. #### `facebook.` * `clientID: string` - your app id as provided by the [App Dashboard](https://developers.facebook.com/apps/). * `clientSecret: string` - your app secret from the App Dashboard. * `scope: string[]` - an array of strings listing [Facebook API scopes](https://developers.facebook.com/docs/facebook-login/permissions/). * `redirectUri: string` - the redirect URI for your app, must be the same as in the App Dashboard. * `authUrl: ?string` - see above, defaults to `https://www.facebook.com/v3.0/dialog/oauth`. * `tokenUrl: ?string` - see above, defaults to `https://graph.facebook.com/v3.0/oauth/access_token` * `responseType` - see above, defaults to `code`. #### `github.` * `clientID: string` - your app id as provided in the [Github Developer Settings](https://github.com/settings/developers). * `clientSecert: string` - your app secret from the Developer Settings. * `scope: string[]` - an array of strings listing [Github API scopes](https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/). * `redirectUri: string` - the redirect URI for your app, must be the same as in the Developer Settings. * `authUrl: ?string` - see above, defaults to `https://github.com/login/oauth/authorize`. * `tokenUrl: ?string` - see above, defaults to `https://github.com/login/oauth/access_token` * `allowSignup: ?boolean` - determines whether the user will be able to sign up to Github while authorizing the app, defaults to `true`. ### Methods #### `oauth.google.` * `redirect() => string` - parses the config options and returns a redirect URL to the user consent screen. * `token(code: string) => Promise` - exchanges the auth code in the first argument for an access token. Returns a promise which resolves to the response from Google. #### `oauth.facebook.` * `redirect(state: ?string) => string` - parses the config and returns a redirect URL to the user consent screen. You can provide a state string to secure your app against CSRF ([see here for details](https://developers.facebook.com/docs/facebook-login/security/#stateparam)). * `token(code: string) => Promise` - exchanges the auth code in the first argument for an access token. Returns a promise which resolves to the repsonse from Facebook. #### `oauth.github.` * `redirect(state: ?string) => string` - parses the config and returns a redirect URL to the user consent screen. You can provide a state string to secure your app against CSRF. * `token(code: string, state: ?string) => Promise` - exchanges the auth code in the first argument for an access token. Returns a promise which resolves to the repsonse from Facebook. ================================================ FILE: packages/hadron-oauth/index.ts ================================================ import { IOAuthConfig, IContainer } from './src/types'; import facebookRedirect from './src/facebook/redirect'; import googleRedirect from './src/google/redirect'; import githubRedirect from './src/github/redirect'; import facebookToken from './src/facebook/token'; import googleToken from './src/google/token'; import githubToken from './src/github/token'; export default { facebook: { redirect: facebookRedirect, token: facebookToken, }, google: { redirect: googleRedirect, token: googleToken, }, github: { redirect: githubRedirect, token: githubToken, }, }; const oauthProvider = (oauthConfig: IOAuthConfig) => ({ facebook: { redirect: (state?: string) => facebookRedirect(oauthConfig, state), token: (code: string) => facebookToken(code, oauthConfig), }, google: { redirect: () => googleRedirect(oauthConfig), token: (code: string) => googleToken(code, oauthConfig), }, github: { redirect: (state?: string) => githubRedirect(oauthConfig), token: (code: string, state?: string) => githubToken(code, oauthConfig, state), }, }); export const register = (container: IContainer, config: any) => { const oauthConfig = config.oauth as IOAuthConfig; container.register('oauth', oauthProvider(oauthConfig)); }; export { IOAuthConfig, IContainer } from './src/types'; ================================================ FILE: packages/hadron-oauth/package.json ================================================ { "name": "@brainhubeu/hadron-oauth", "version": "1.0.2", "description": "Hadron module for authorisation from OAuth2 providers.", "main": "dist/index.js", "license": "MIT", "author": "Brainhub", "keywords": [ "hadron", "hadron-oauth" ], "scripts": { "prepare": "tsc", "test": "mocha -r ts-node/register src/__tests__/*" }, "devDependencies": { "@types/nock": "9.1.3", "chai": "4.1.2", "eslint-config-brainhub": "1.8.1", "mocha": "5.2.0", "nock": "9.3.3", "ts-node": "6.1.1" }, "dependencies": { "@types/node-fetch": "2.1.1", "node-fetch": "2.1.2" }, "publishConfig": { "access": "public" } } ================================================ FILE: packages/hadron-oauth/src/__tests__/facebook.ts ================================================ import { expect } from 'chai'; import * as nock from 'nock'; import * as url from 'url'; import generateRedirectUrl from '../facebook/redirect'; import requestToken from '../facebook/token'; const config = { facebook: { clientID: 'keyboard-cat', clientSecret: 'shhh', redirectUri: 'http://localhost:8080', scope: ['profile'], }, }; // @TODO: Errors and bad inputs. describe('facebook', function() { it('generates a valid redirect url', function() { const uri = generateRedirectUrl(config); expect(url.parse(uri)).to.be.ok; }); it('exchanges auth code for url', function(done) { nock('https://graph.facebook.com') .get('/v3.0/oauth/access_token') .query((body: any) => { if ( body.code && body.client_id && body.client_secret && body.redirect_uri ) { return true; } }) .reply(200, { access_token: 'bearhaslanded' }); requestToken('code', config).then((json) => { expect(json.access_token).to.be.equal('bearhaslanded'); done(); }); }); }); ================================================ FILE: packages/hadron-oauth/src/__tests__/formatQueryString.ts ================================================ import { expect } from 'chai'; import formatQueryString from '../util/formatQueryString'; describe('utils', function() { it('formatQueryString should format an object into a correct url query string', function() { const obj = { a: 5, b: 'potato' }; const query = formatQueryString(obj); expect(query).to.be.equal('a=5&b=potato&'); }); }); ================================================ FILE: packages/hadron-oauth/src/__tests__/github.ts ================================================ import { expect } from 'chai'; import * as nock from 'nock'; import * as url from 'url'; import generateRedirectUrl from '../github/redirect'; import requestToken from '../github/token'; const config = { github: { clientID: 'keyboard-cat', clientSecret: 'shhh', redirectUri: 'http://localhost:8080', scope: ['user'], }, }; // @TODO: Errors and bad inputs. describe('github', function() { it('generates a valid redirect url', function() { const uri = generateRedirectUrl(config); expect(url.parse(uri)).to.be.ok; }); it('exchanges auth code for url', function(done) { nock('https://github.com') .post('/login/oauth/access_token', (body: any) => { if ( body.code && body.client_id && body.client_secret && body.redirect_uri ) { return true; } }) .reply(200, { access_token: 'bearhaslanded' }); requestToken('code', config).then((json) => { expect(json.access_token).to.be.equal('bearhaslanded'); done(); }); }); }); ================================================ FILE: packages/hadron-oauth/src/__tests__/google.ts ================================================ import { expect } from 'chai'; import * as nock from 'nock'; import * as url from 'url'; import generateRedirectUrl from '../google/redirect'; import requestToken from '../google/token'; const config = { google: { clientID: 'keyboard-cat', clientSecret: 'shhh', redirectUri: 'http://localhost:8080', scope: ['https://www.googleapis.com/auth/userinfo.profile'], }, }; // @TODO: Errors and bad inputs. describe('google', function() { it('generates a valid redirect url', function() { const uri = generateRedirectUrl(config); expect(url.parse(uri)).to.be.ok; }); it('exchanges auth code for url', function(done) { nock('https://www.googleapis.com') .post('/oauth2/v4/token', (body: any) => { if ( body.code && body.client_id && body.client_secret && body.grant_type ) { return true; } }) .reply(200, { access_token: 'bearhaslanded' }); requestToken('code', config).then((json) => { expect(json.access_token).to.be.equal('bearhaslanded'); done(); }); }); }); ================================================ FILE: packages/hadron-oauth/src/facebook/redirect.ts ================================================ import { IContainer, IOAuthConfig } from '../types'; import { FACEBOOK_AUTH_URL } from '../util/constants'; import formatQueryString from '../util/formatQueryString'; export default (config: IOAuthConfig, state?: string): string => { const host = config.facebook.authUrl || FACEBOOK_AUTH_URL; const query = { client_id: config.facebook.clientID, redirect_uri: config.facebook.redirectUri, scope: config.facebook.scope.join(','), response_type: config.facebook.responseType || 'code', state: state || '', }; const queryString = formatQueryString(query); return `${host}?${queryString}`; }; ================================================ FILE: packages/hadron-oauth/src/facebook/token.ts ================================================ import fetch from 'node-fetch'; import { IOAuthConfig } from '../types'; import formatQueryString from '../util/formatQueryString'; import { FACEBOOK_TOKEN_URL } from '../util/constants'; export default (code: string, config: IOAuthConfig): Promise => { const host = config.facebook.tokenUrl || FACEBOOK_TOKEN_URL; const query = { code, client_id: config.facebook.clientID, client_secret: config.facebook.clientSecret, redirect_uri: config.facebook.redirectUri, }; const queryString = formatQueryString(query); return fetch(`${host}?${queryString}`).then((res) => res.json()); }; ================================================ FILE: packages/hadron-oauth/src/github/redirect.ts ================================================ import { IContainer, IOAuthConfig } from '../types'; import { GITHUB_AUTH_URL } from '../util/constants'; import formatQueryString from '../util/formatQueryString'; export default (config: IOAuthConfig, state?: string): string => { const host = config.github.authUrl || GITHUB_AUTH_URL; const query = { client_id: config.github.clientID, redirect_uri: config.github.redirectUri, scope: config.github.scope.join(','), state: state || '', }; const queryString = formatQueryString(query); return `${host}?${queryString}`; }; ================================================ FILE: packages/hadron-oauth/src/github/token.ts ================================================ import fetch from 'node-fetch'; import { IOAuthConfig } from '../types'; import { GITHUB_TOKEN_URL } from '../util/constants'; export default ( code: string, config: IOAuthConfig, state?: string, ): Promise => { const host = config.github.tokenUrl || GITHUB_TOKEN_URL; const query = { code, state, client_id: config.github.clientID, client_secret: config.github.clientSecret, redirect_uri: config.github.redirectUri, }; return fetch(host, { method: 'POST', headers: { 'content-type': 'application/json', Accept: 'application/json', }, body: JSON.stringify(query), }).then((res) => res.json()); }; ================================================ FILE: packages/hadron-oauth/src/google/redirect.ts ================================================ import { IContainer, IOAuthConfig } from '../types'; import { GOOGLE_AUTH_URL } from '../util/constants'; import formatQueryString from '../util/formatQueryString'; export default (config: IOAuthConfig): string => { const host = config.google.authUrl || GOOGLE_AUTH_URL; const query = { client_id: config.google.clientID, redirect_uri: config.google.redirectUri, scope: config.google.scope.join(' '), response_type: config.google.responseType || 'code', }; const queryString = formatQueryString(query); return `${host}?${queryString}`; }; ================================================ FILE: packages/hadron-oauth/src/google/token.ts ================================================ import fetch from 'node-fetch'; import { IOAuthConfig } from '../types'; import formatQueryString from '../util/formatQueryString'; import { GOOGLE_TOKEN_URL } from '../util/constants'; export default (code: string, config: IOAuthConfig): Promise => { const host = config.google.tokenUrl || GOOGLE_TOKEN_URL; const query = { code, client_id: config.google.clientID, client_secret: config.google.clientSecret, redirect_uri: config.google.redirectUri, grant_type: config.google.grantType || 'authorization_code', }; return fetch(host, { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded', }, body: formatQueryString(query), }).then((res) => res.json()); }; ================================================ FILE: packages/hadron-oauth/src/types.ts ================================================ export interface IContainer { register: (key: string, value: any, lifecycle?: string) => any; take: (key: string) => any; keys: () => string[]; } export interface IOAuthConfig { google?: { clientID: string; clientSecret: string; scope: string[]; redirectUri: string; authUrl?: string; tokenUrl?: string; responseType?: string; grantType?: string; }; facebook?: { clientID: string; clientSecret: string; redirectUri: string; scope: string[]; authUrl?: string; tokenUrl?: string; responseType?: string; }; github?: { clientID: string; clientSecret: string; redirectUri: string; scope: string[]; authUrl?: string; tokenUrl?: string; allowSignup?: boolean; }; } export interface IQueryObject { [index: string]: any; } ================================================ FILE: packages/hadron-oauth/src/util/constants.ts ================================================ export const GOOGLE_AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth'; export const GOOGLE_TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token'; export const FACEBOOK_AUTH_URL = 'https://www.facebook.com/v3.0/dialog/oauth'; export const FACEBOOK_TOKEN_URL = 'https://graph.facebook.com/v3.0/oauth/access_token'; export const GITHUB_AUTH_URL = 'https://github.com/login/oauth/authorize'; export const GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token'; ================================================ FILE: packages/hadron-oauth/src/util/formatQueryString.ts ================================================ import { IQueryObject } from '../types'; export default (query: IQueryObject): string => { return Object.keys(query).reduce((acc, i) => { return `${acc}${i}=${query[i]}&`; }, ''); }; ================================================ FILE: packages/hadron-oauth/tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "target": "es6", "noImplicitAny": true, "noEmitOnError": true, "moduleResolution": "node", "sourceMap": true, "outDir": "dist", "baseUrl": ".", "paths": { "*": ["./*"] }, "emitDecoratorMetadata": true, "experimentalDecorators": true, "inlineSources": true, "lib": ["es2017", "dom"] }, "include": ["src/**/*.*", "index.ts", "LICENSE"], "exclude": ["node_modules", "**/__tests__/**", "**/*/*.d.ts"] } ================================================ FILE: packages/hadron-serialization/LICENSE ================================================ MIT License Copyright (c) 2018 Brainhub Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/hadron-serialization/README.md ================================================ ## Installation ```bash npm install @brainhubeu/hadron-serialization --save ``` [More info about installation](http://hadron-docs.dev.brainhub.pl/core/#installation) ## Overview Serializer allows You to quickly and easy shape and parse data way You want it. You just need to create schema (in json file, or as simple object) and You are ready to go!s ## Initializing as Hadron package Pass package as an argument for hadron bootstrapping function: ```javascript // ... importing and initializing other components hadron(expressApp, [require('@brainhubeu/hadron-serialization')], config); ``` That way, You should be able to get it from [Container](http://hadron-docs.dev.brainhub.pl/core/#dependency-injection) like that: ```javascript const serializer = container.take('serializer'); serializer.addSchema({ name: 'User', properties: [ ... ], }); // ... const data = { ... }; serializer.serialize(data, 'User'); // or const data = new User(); serializer.serialize(data); ``` ## Initializing without Hadron Just import `serializerProvider` function from package and pass there Your [schemas](#schema) and parsers. ```javascript const serializerProvider = require('@brainhubeu/hadron-serialization'); const serializer = serializerProvider({ schemas: mySchemas, parsers: { superParser: (value) => `Super ${value}`, }, // ... }); ``` ## Configuration If You are using hadron application, You just need to add to it's config schemas and set of parsers: ```javascript const config = { // ... serializer: { schemas: [ ... ], parser: [ ... ], } }; ``` If You are using TypeScript, You can just implement exported interface `ISerializerConfig` ```typescript interface ISerializerConfig { parsers?: object; schemas?: ISerializationSchema[]; } ``` ## Usage Serializer contain three methods. ```javascript serialize(data, groups, schemaName); ``` * `data` - object we want to serialize * `groups` - optional array of access [groups](#groups), on default `[]` * `schemaName` - name of schema, on default name of passed object ```javascript addSchema(schemaObj); ``` * `schemaObj` - [schema](#schema) object we want to add ```javascript addParser(parser, name); ``` Adds parser that can be used in schemas, where: * `parser` is a method * `name` is name under which parser will be available ## Schema Schema is basic structure, that allows You to easily define your desired object. You can provider them as `json` file. F.e. ```json { "name": "User", "properties": [ { "name": "name", "type": "string" }, { "name": "address", "type": "string", "groups": ["admin"] }, { "name": "money", "type": "number", "parsers": ["currency"], "groups": ["admin"] }, { "name": "friends", "type": "array", "properties": [ { "name": "name", "type": "string" }, { "name": "profession", "type": "string", "groups": ["admin"] }, { "name": "salary", "type": "number", "parsers": ["currency"] } ] } ] } ``` Each schema should contain `name`, which will be its identifier, and `properties` which should be an array of fields of defined schema. All properties that are not defined in schema, will be excluded from the serialized data result. If You are using TypeScript, You can just implement exported interface `ISerializationSchema`: ```typescript interface ISerializationSchema { name: string; properties: IProperty[]; } ``` ## Property Each property should contain such fields: * `name` - (required) name of the field * `type` - (required) one of such types: * `string` * `number` * `bool` * `array` * `object` * `groups` - array of strings, that will define accessibility to this field ([link](#groups)). If empty, such field if public and will be returned always. * `parsers` - array of parsers name, that should be run on this field, before it's returned * `properties` - array of properties, that are required in case of type `object` and `array` * `serializedName` - name of field after serialization If You are using TypeScript, You can just implement exported interface `IProperty`: ```typescript interface IProperty { name: string; type: string; serializedName?: string; groups?: string[]; parsers?: string[]; properties?: IProperty[]; } ``` ## Groups While defining schema, You can add `groups` parameter to properties. That way, while serializing data, You can specify serialization group. ```javascript const schema = { name: 'User', properties: [ // ... { name: 'firstname', type: 'string' }, { name: 'lastname', type: 'string', groups: ['friends'] } ], } serializer.addSchema(schema); // ... const data = { firstname: 'John', lastname: 'Doe', id: 481, }; console.log(serializer.serialize(data, [], 'User'); // { 'firstname': 'John' } console.log(serializer.serialize(data, ['friends'], 'User'); // { 'firstname': 'John', 'lastname': 'Doe' } ``` ================================================ FILE: packages/hadron-serialization/index.ts ================================================ import * as interfaces from './src/types'; import { CONTAINER_NAME } from './src/constants'; import schema from './src/schema-provider'; import serializerProvider from './src/serializer'; import { IContainer } from '@brainhubeu/hadron-core'; export default serializerProvider; export * from './src/constants'; export const schemaProvider = schema; export type ISerializer = interfaces.ISerializer; export type ISerializerConfig = interfaces.ISerializerConfig; export type ISerializationSchema = interfaces.ISerializationSchema; export type IProperty = interfaces.IProperty; export const register = ( container: IContainer, config: interfaces.IHadronSerializerConfig, ) => { const serializerConfig: interfaces.ISerializerConfig = { ...config.serializer, }; const serializer = serializerProvider(serializerConfig); container.register(CONTAINER_NAME, serializer); }; ================================================ FILE: packages/hadron-serialization/package.json ================================================ { "name": "@brainhubeu/hadron-serialization", "version": "1.0.0", "description": "Hadron serialization module", "main": "dist/index.js", "files": [ "dist", "./LICENSE" ], "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "prepare": "tsc" }, "keywords": [ "hadron", "brainhub", "hadron-serialization" ], "author": "Brainhub", "license": "MIT", "dependencies": { "@brainhubeu/hadron-core": "^1.0.0", "@brainhubeu/hadron-json-provider": "^1.0.0" }, "publishConfig": { "access": "public" } } ================================================ FILE: packages/hadron-serialization/src/__tests__/mocks.ts ================================================ import { IProperty, ISerializationSchema, ISerializerConfig } from '../types'; export const fruitConfiguration = { name: 'Fruit', properties: [ { name: '_id', type: 'string', serializedName: 'ID' } as IProperty, { name: 'name', type: 'string' } as IProperty, { name: 'price', groups: ['common', 'seller'], type: 'number', } as IProperty, { groups: ['seller'], name: 'funkyName', parsers: ['funkyParser'], type: 'string', } as IProperty, { name: 'flavour', properties: [ { name: 'bitterness', type: 'number', } as IProperty, { name: 'sweetness', type: 'number', } as IProperty, ], type: 'object', }, ], } as ISerializationSchema; export const carConfiguration = { name: 'Car', properties: [ { name: 'name', groups: ['common', 'seller'], type: 'string' } as IProperty, { name: 'price', groups: ['common', 'seller'], type: 'number', } as IProperty, { groups: ['seller'], name: 'type', type: 'string', } as IProperty, ], } as ISerializationSchema; export const options = { parsers: { funkyParser: (data: any) => `funky ${data}`, }, } as ISerializerConfig; ================================================ FILE: packages/hadron-serialization/src/__tests__/schema-provider.ts ================================================ import { expect } from 'chai'; import * as sinon from 'sinon'; import { ISerializationSchema, ISerializerConfig } from '../types'; import * as jsonProvider from '@brainhubeu/hadron-json-provider'; import schemaProvider from '../schema-provider'; describe('schemaProvider', () => { it('should try to locate files in given location', () => { const mockResponse: any = [ { name: 'Test', properties: [] }, { name: 'Second', properties: [] }, ]; const jsonProviderStub = sinon .stub(jsonProvider, 'default') .callsFake(() => Promise.resolve(mockResponse)); const result = schemaProvider(['./test/location']); jsonProviderStub.restore(); return expect(result).to.eventually.be.eql(mockResponse); }); }); ================================================ FILE: packages/hadron-serialization/src/__tests__/serializer.ts ================================================ import { expect } from 'chai'; import * as sinon from 'sinon'; import serializerProvider, { hasIntersection, serialize, serializeEntry, } from '../serializer'; import { carConfiguration, fruitConfiguration } from './mocks'; import { IProperty, ISerializer } from '../types'; describe('serializer', () => { describe('hasIntersection', () => { it('should return true if at least one element is common for both arrays', () => { expect(hasIntersection([1, 2, 3], [3, 4, 5])).to.eql(true); }); it('should return false if no elements are common for both arrays', () => { expect(hasIntersection([1, 2, 3], [4, 5, 6])).to.eql(false); }); it('should return false if one of arrays are empty', () => { expect(hasIntersection([1, 2, 3], [])).to.eql(false); }); }); describe('serializeEntry()', () => { it('should run parse method for defined type', () => { const parsers = { number: sinon.spy(Number), }; const property = { name: 'price', type: 'number', }; serializeEntry('123', ['common'], property, parsers); expect(parsers.number.calledWith('123')); }); it('should run parse method for defined type', () => { const parsers = { number: sinon.spy(Number), }; const property = { name: 'price', type: 'number', }; serializeEntry('123', ['common'], property, parsers); expect(parsers.number.calledWith('123')); }); }); describe('serialize()', () => { it('should exclude properties not belonging to configuration', () => { const data = { absent: "I shouldn't be here", name: 'Civic', price: '12000', type: 'hatchback', }; expect( serialize(data, ['seller'], carConfiguration.properties, {}), ).to.not.contain.keys(['absent']); }); it('should include properties belonging to group', () => { const data = { name: 'Civic', price: '12000', type: 'hatchback', }; expect( serialize(data, ['common'], carConfiguration.properties, {}), ).to.contain.keys(['name', 'price']); }); it('should run parsers before returning value', () => { const data = { name: 'Civic', price: '12000', type: 'hatchback', }; const parsers = { number: sinon.spy(Number) }; serialize(data, ['common'], carConfiguration.properties, parsers); expect(parsers.number.calledWith('12000')).to.be.eql(true); }); it('should include nested parameters', () => { const data = { flavour: { bitterness: '2', sweetness: '12', }, name: 'Pear', price: '20', }; const result = serialize( data, ['common'], fruitConfiguration.properties, {}, ); expect(result).to.have.property('flavour'); }); it('should parse nested parameters', () => { const data = { flavour: { bitterness: '2', sweetness: '12', }, name: 'Pear', price: '20', }; const parsers = { number: sinon.spy(Number), }; serialize(data, ['common'], fruitConfiguration.properties, parsers); expect(parsers.number.calledThrice).to.eql(true); }); it('should set key as defined serializedName', () => { const data = { _id: '12', flavour: { bitterness: '2', sweetness: '12', }, name: 'Pear', price: '20', }; const result = serialize( data, ['common'], fruitConfiguration.properties, {}, ); expect(result).to.have.property('ID'); }); it('should serialize object with given properties', () => { const serializerProperties = { name: 'objectProperty', properties: [ { name: 'String', type: 'string' }, { name: 'Number', type: 'number' }, ], type: 'object', } as IProperty; const data = { objectProperty: { String: '123', Number: 456, }, }; expect( serialize(data, ['common'], [serializerProperties], {}), ).to.be.deep.equal({ objectProperty: { Number: 456, String: '123', }, }); }); it('should exclude properties from other gorups from object serialization', () => { const serializerProperties = { name: 'objectProperty', properties: [ { name: 'String', type: 'string' }, { name: 'Number', type: 'number', groups: ['uncommon'] }, ], type: 'object', } as IProperty; const data = { objectProperty: { String: '123', Number: 456, }, }; expect( serialize(data, ['common'], [serializerProperties], {}), ).to.be.deep.equal({ objectProperty: { String: '123', }, }); }); it('should serialize array with given properties and groups', () => { const serializerProperties = { name: 'arrayProperties', properties: [ { name: 'String', type: 'string', groups: ['common'] }, { name: 'Number', type: 'number', groups: ['uncommon'] }, ], type: 'array', } as IProperty; const data = { arrayProperties: [ { String: '123', Number: 456 }, { String: '222', Number: 123 }, ], }; expect( serialize(data, ['common'], [serializerProperties], {}), ).to.be.deep.equal({ arrayProperties: [ { String: '123', }, { String: '222', }, ], }); }); it('should parse array of serializable elements', () => { const serializerProperties = { name: 'arrayProperties', properties: [ { name: 'CommonString', type: 'string' }, { name: 'String', type: 'string' }, ], type: 'array', } as IProperty; const data = { arrayProperties: [ { CommonString: 'lorem ipsum', String: 'dolor', NoString: 'sit amet', Number: 12, }, ], }; expect( serialize(data, ['common'], [serializerProperties], {}), ).to.be.deep.equal({ arrayProperties: [ { CommonString: 'lorem ipsum', String: 'dolor', }, ], }); }); it("should exclude parameters from array, that doesn't belong to group", () => { const serializerProperties = { name: 'arrayProperties', properties: [ { name: 'CommonString', type: 'string' }, { name: 'String', type: 'string', groups: ['common'] }, { name: 'Number', type: 'number', groups: ['uncommon'] }, ], type: 'array', } as IProperty; const data = { arrayProperties: [ { CommonString: 'lorem ipsum', String: 'dolor', NoString: 'sit amet', Number: 12, }, ], }; expect( serialize(data, ['common'], [serializerProperties], {}), ).to.be.deep.equal({ arrayProperties: [ { CommonString: 'lorem ipsum', String: 'dolor', }, ], }); }); }); describe('serializerProvider()', () => { it('should add configuration dynamically', () => { const serializer: ISerializer = serializerProvider({}); serializer.addSchema(carConfiguration); const data = { name: 'Civic', price: '12000', type: 'hatchback', }; expect( serializer.serialize(data, ['common'], 'Car'), ).to.eventually.contain.keys(['name', 'price']); }); it('should add parser dynamically', () => { const serializer: ISerializer = serializerProvider({ schemas: [ { name: 'Test', properties: [ { name: 'parsesParams', parsers: ['testParser'], type: 'Something', }, ], }, ], }); const parserSpy = sinon.spy(); serializer.addParser(parserSpy, 'testParser'); const data = { parsesParams: 'smth', }; return serializer .serialize(data, ['common'], 'Test') .then(() => expect(parserSpy.called).to.eql(true)); }); }); }); ================================================ FILE: packages/hadron-serialization/src/constants.ts ================================================ export const DATA_TYPE = { NUMBER: 'number', BOOL: 'bool', STRING: 'string', OBJECT: 'object', ARRAY: 'array', }; // name in container export const CONTAINER_NAME = 'serializer'; ================================================ FILE: packages/hadron-serialization/src/declarations.d.ts ================================================ declare module '@hadron/json-provider'; ================================================ FILE: packages/hadron-serialization/src/schema-provider.ts ================================================ import jsonProvider from '@brainhubeu/hadron-json-provider'; export default (paths: string[]) => { if (paths.length >= 0) { try { return jsonProvider(paths, ['json'], true); } catch (error) { return Promise.reject(new Error(`Incorrect configuration: ${error}`)); } } return Promise.resolve([]); }; ================================================ FILE: packages/hadron-serialization/src/serializer.ts ================================================ import { IProperty, ISerializationSchema, ISerializer, ISerializerConfig, } from './types'; import { DATA_TYPE } from './constants'; const defaultParsers = { [DATA_TYPE.BOOL]: Boolean, [DATA_TYPE.NUMBER]: Number, [DATA_TYPE.STRING]: String, }; /** * Searches for property * * @param key "name" of current property * @param properties list of available properties */ export const getPropertyForKey = ( key: string, properties: IProperty[] = [], ): IProperty => properties.find((property: IProperty) => property.name === key); /** * Searches for parser * * @param names array of names for parsers, one of them should be "type" of property, * other are additional parsers defined in property * @param availableParsers list of available parsers */ export const getParsers = (names: string[], availableParsers: object) => { const namesSet = new Set(names); return (Object as any) .entries(availableParsers) .filter(([key, parser]: [string, any]) => namesSet.has(key)) .map(([key, parser]: [string, any]) => parser); }; /** * Searches for configration * * @param type "name" of current serialization * @param configurations list of available configurations */ export const getConfigurationForType = ( type: string, configurations: ISerializationSchema[], ) => configurations.find( (configuration: ISerializationSchema) => configuration.name === type, ); /** * Checks if given arrays have at least one common memer * * @param firstArray * @param secondArray */ export const hasIntersection = ( firstArray: any[], secondArray: any[], ): boolean => { const secondArraySet = new Set(secondArray); return ( firstArray.filter((value: any) => secondArraySet.has(value)).length > 0 ); }; /** * Serializes object with given properties, excluding ones not existing in properties array * * @param data * @param groups serialization groups * @param properties list of available properties * @param parsers list of available parsers */ export const serialize = ( data: object, groups: string[], properties: IProperty[], parsers: object, ) => (Object as any) .entries(data) // exclude properties not present in schema or not containing proper group .filter(([key, value]: [string, any]) => { const property = getPropertyForKey(key, properties); // if parameter has no groups, its public return ( property && (!property.groups || hasIntersection(property.groups, groups)) ); }) .reduce((result: any, [key, value]: [string, any]) => { const property = getPropertyForKey(key, properties); const propertyKey = property.serializedName || key; return Object.assign( result, // tslint:disable:no-use-before-declare { [propertyKey]: serializeEntry(value, groups, property, parsers) }, ); }, {}); /** * Serializes one single entry * * @param value * @param groups current serialization group * @param property instance of property, which contains informations about serialization * @param availableParsers all available parsers */ export const serializeEntry = ( value: any, groups: string[], property: IProperty, availableParsers: object, ): any => { const parsers = getParsers( [...(property.parsers || []), property.type], availableParsers, ); let serializedValue = value; if (property.properties && property.type === DATA_TYPE.OBJECT) { serializedValue = serialize( value, groups, property.properties, availableParsers, ); } if (property.properties && property.type === DATA_TYPE.ARRAY) { serializedValue = value .map((childValue: any) => serialize(childValue, groups, property.properties, availableParsers), ) .reduce( (result: any[], currentValue: any) => [...result, currentValue], [], ); } if (parsers) { serializedValue = parsers.reduce( (accumulator: any, parser: (data: any) => any) => parser(accumulator), serializedValue, ); } return serializedValue; }; /** * Function provider for serialization * * @param configuration array of serialize configurations * @param options options for serializer, like path to configuration files, or additional parsers */ const serializerProvider = (config: ISerializerConfig) => { const parsers = { ...defaultParsers, ...(config.parsers || {}), } as any; const schemas = config.schemas || []; return { addParser: (parser: (data: any) => any, key: string) => { parsers[key] = parser; return parser; }, addSchema: (schema: ISerializationSchema) => { schemas.push(schema); return schemas; }, serialize: (data: any, groups: string[], configurationName?: string) => { // if configurationName is not defined, we are trying to get name from instance of object const name = configurationName || data.constructor.name; const foundConfiguration = schemas.find( (configuration: ISerializationSchema) => configuration.name === name, ); if (!foundConfiguration) { return Promise.reject(new Error('Configuration not found')); } return Promise.resolve(foundConfiguration).then( (schema: ISerializationSchema) => data && serialize(data, groups, schema.properties, parsers), ); }, } as ISerializer; }; export default serializerProvider; ================================================ FILE: packages/hadron-serialization/src/types.ts ================================================ export interface ISerializationSchema { name: string; properties: IProperty[]; } export interface IProperty { name: string; serializedName?: string; groups?: string[]; type: string; parsers?: string[]; properties?: IProperty[]; } export interface ISerializerConfig { parsers?: object; schemas?: ISerializationSchema[]; } export interface IHadronSerializerConfig { serializer: ISerializerConfig; } export interface ISerializer { addParser: (parser: (data: any) => any, key: string) => object; addSchema: (schema: ISerializationSchema) => ISerializationSchema[]; serialize: ( data: any, groups: string[], configurationName?: string, ) => Promise; } ================================================ FILE: packages/hadron-serialization/tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "target": "es6", "noImplicitAny": true, "noEmitOnError": true, "moduleResolution": "node", "sourceMap": true, "outDir": "dist", "baseUrl": ".", "paths": { "*": ["./*"] }, "emitDecoratorMetadata": true, "experimentalDecorators": true, "inlineSources": true, "lib": ["es2017", "dom"] }, "include": ["src/**/*.*", "index.ts", "LICENSE"], "exclude": ["node_modules", "**/__tests__/**", "**/*/*.d.ts"] } ================================================ FILE: packages/hadron-typeorm/LICENSE ================================================ MIT License Copyright (c) 2018 Brainhub Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/hadron-typeorm/README.md ================================================ ## Installation ```bash npm install @brainhubeu/hadron-typeorm --save ``` [More info about installation](/core/#installation) ## Initializing Pass package as an argument for hadron bootstrapping function: ```javascript const hadronTypeOrm = require('@brainhubeu/hadron-typeorm'); // ... importing and initializing other components hadron(expressApp, [hadronTypeOrm], config).then(() => { console.log('Hadron with typeORM initialized'); }); ``` ## Connecting to database You can set up a new connection using [connection object](http://typeorm.io/#/connection). ```javascript { connectionName: string, type: string, host: string, port: number, username: string, password: string, database: string entitySchemas: entities, synchronize: bool, } ``` * `connectionName` - string that identifies this connection * `type` - string that defines type of database, f.e. mysql, mariadb, postgres, sqlite, mongodb, * `host` - url to database, * `port` - port of database, * `username` - username of account to databse, * `password` - password to database, * `database` - name of database * `entities` - array of classes that defines models * `entitySchemas` - in case that You are describing models with schemas, put those in this parameter * `synchronize` - parameter that defines if database should be automatically synchronized with models Also all other parameters available in typeOrm are available. Please take a look at [TypeORM documentation](https://github.com/typeorm/typeorm#creating-a-connection-to-the-database) ## Including database connection in hadron _NOTE: Also events aren't included in this section so logging into the console is done using setTimeout._ Since we have our connection, we need to include it inside our hadron constructor's config object. ```javascript const hadronTypeOrm = require('@brainhubeu/hadron-typeorm'); const config = { connection: { /* connection object */ }, }; hadron(expressApp, [hadronTypeOrm], config).then((container) => { console.log('Initialized connection:', container.take('connection')); }); ``` ## Entities Let's assume we want to have a simple table **user** | Field | Type | | --------- | ------- | | 🔑 id | int | | firstName | varchar | | lastName | varchar | We have two options while defining our `entities`. ### Class + Decorators ```typescript import { Entity, Column, PrimaryColumn } from 'typeorm'; @Entity() export class Photo { @PrimaryGeneratedColumn() @Column() id: number; @Column() firstName: string; @Column() lastName: string; } ``` When using this method, while creating connection to database, those classes should be in `entities` parameter. ### Schema Way ```javascript // entity/User.js module.exports = { name: 'User', columns: { id: { primary: true, type: 'int', generated: true, }, firstName: { type: 'varchar', }, lastName: { type: 'varchar', }, }, }; ``` When using this method, while creating connection to database, those schemas should be in `entitySchemas` parameter. For more details about defining models, please take a look at [TypeORM documentation](http://typeorm.io/#/entities). Especially section about [available types](http://typeorm.io/#/entities/column-types) for each database distribution ## Injecting entities into hadron To include our entities in hadron, we simply need to include them in our config object. Let's modify the code that we were using to initialize hadron: ```javascript const hadronTypeOrm = require('@brainhubeu/hadron-typeorm'); const user = require('./entity/User'); const config = { connection, entities: [user], }; hadron(expressApp, [hadronTypeOrm], config).then((container) => { console.log( 'userRepository available:', container.take('userRepository') !== null, ); // User entity should be declared under userRepository key and // will be available as a typeORM repository }); ``` Repository key in Container depends from name of schema/class and is builded in such way: `Repository` Examples: ``` User = userRepository SuperUser = superUserRepository loremIpsumDolor = loremIpsumDolorRepository ``` ## Repositories Generater repositories contain same methods as ones from TypeORM. Please check them out here: [http://typeorm.io/#/working-with-entity-manager](http://typeorm.io/#/working-with-entity-manager) ## Troubleshooting ### I can' connect to database: * make sure that connection config has valid data and there is existing database with specified name ### There are no tables in my database * There are few possible reasons for that. Firstly check if parameter `synchronize` in configuration is set to true. * Then make sure that connection configuration contains `entities`/`entitySchemas` fields. * Remember, if You are using class definition of models, You need to put them in `entities` parameter, otherwise (schema method) in `entitySchemas` ### There is an information that I am missing a driver * If you decided which database You want to use, You need to add a proper driver to your dependencies. For more details check TypeORM [README](https://github.com/typeorm/typeorm#installation) file ================================================ FILE: packages/hadron-typeorm/index.ts ================================================ import { connect } from './src/connectionHelper'; import * as constants from './src/constants'; import { IHadronTypeormConfig } from './src/types'; import { IContainer } from '@brainhubeu/hadron-core'; export const register = ( container: IContainer, config: IHadronTypeormConfig, ): Promise => connect(container, config); export { connect, constants, IHadronTypeormConfig }; ================================================ FILE: packages/hadron-typeorm/package.json ================================================ { "name": "@brainhubeu/hadron-typeorm", "version": "1.0.3", "description": "Hadron typeorm module", "main": "dist/index.js", "files": [ "dist", "./LICENSE" ], "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "prepare": "tsc" }, "keywords": [ "hadron", "typeorm", "hadron-typeorm" ], "author": "Brainhub", "license": "MIT", "dependencies": { "@brainhubeu/hadron-core": "^1.0.0", "typeorm": "0.1.16" }, "publishConfig": { "access": "public" } } ================================================ FILE: packages/hadron-typeorm/src/__tests__/connectionHelper.ts ================================================ import { assert } from 'chai'; import * as sinon from 'sinon'; import * as typeorm from 'typeorm'; import { Container } from '@brainhubeu/hadron-core'; import { connect } from '../connectionHelper'; import { CONNECTION } from '../constants'; import { Team } from './mocks/entity/Team'; import { User } from './mocks/entity/User'; import { UserStatus } from './mocks/entity/UserStatus'; import userSchema from './mocks/schema/User'; const connection: typeorm.ConnectionOptions = { name: 'mysql-connection', type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'my-secret-pw', database: 'test', entities: [Team, User, UserStatus], }; describe('TypeORM connection helper', () => { const createConnectionStub = sinon.stub(typeorm, 'createConnection'); createConnectionStub.returns( new Promise((resolve) => { resolve(new typeorm.Connection(connection)); }), ); const getRepositoryStub = sinon.stub( typeorm.Connection.prototype, 'getRepository', ); getRepositoryStub.returns(true); beforeEach(() => { Container.register('connection', null); Container.register('teamRepository', null); Container.register('userRepository', null); }); after(() => { createConnectionStub.restore(); getRepositoryStub.restore(); }); it('should return connection', () => connect(Container, { connection }).then((connection: any) => { return assert(connection instanceof typeorm.Connection); })); it('should register connection to container', () => { connect(Container, { connection }).then((connection: any) => { assert(Container.take(CONNECTION) instanceof typeorm.Connection); }); }); it('should register Team repository to container as teamRepository', () => connect(Container, { connection }).then((connection: any) => assert(Container.take('teamRepository')), )); it('should register UserStatus repository to container as userstatusRepository', () => connect(Container, { connection }).then((connection: any) => assert(Container.take('userstatusRepository')), )); it('should register User repository from javascript schema to container as userRepository', () => connect(Container, { connection: { ...connection, entities: [], entitySchemas: [userSchema], }, }).then((connection: any) => assert(Container.take('userRepository')))); }); ================================================ FILE: packages/hadron-typeorm/src/__tests__/mocks/entity/Team.ts ================================================ import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; import { User } from './User'; @Entity() export class Team { @PrimaryGeneratedColumn() public id: number; @Column({ type: 'text' }) public name: string; @OneToMany((type) => User, (user) => user.team) public users: User[]; } ================================================ FILE: packages/hadron-typeorm/src/__tests__/mocks/entity/User.ts ================================================ import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; import { Team } from './Team'; import { UserStatus } from './UserStatus'; @Entity() export class User { @PrimaryGeneratedColumn() public id: number; @Column({ type: 'text' }) public name: string; @ManyToOne((type) => Team, (team) => team.users) public team: Team; @ManyToOne((type) => UserStatus, (userStatus) => userStatus.users) public userStatus: UserStatus; } ================================================ FILE: packages/hadron-typeorm/src/__tests__/mocks/entity/UserStatus.ts ================================================ import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; import { User } from './User'; @Entity() export class UserStatus { @PrimaryGeneratedColumn() public id: number; @Column({ type: 'text' }) public name: string; @OneToMany((type) => User, (user) => user.status) public users: User[]; } ================================================ FILE: packages/hadron-typeorm/src/__tests__/mocks/schema/User.ts ================================================ export default { name: 'User', columns: { id: { primary: true, type: 'int', generated: true, }, name: { type: 'string', }, team: { type: 'string', }, }, }; ================================================ FILE: packages/hadron-typeorm/src/connectionHelper.ts ================================================ import { createConnection, Connection, EntityOptions } from 'typeorm'; import { CONNECTION } from './constants'; import { IContainer } from '@brainhubeu/hadron-core'; import { IHadronTypeormConfig } from './types'; const repositoryName = (name: string) => `${name.toLowerCase()}Repository`; const registerRepositories = ( container: IContainer, connection: Connection, entities: Array, ) => { entities.forEach((entity: string | EntityOptions) => { const name: string = typeof entity === 'string' ? entity : entity.name; container.register(repositoryName(name), connection.getRepository(name)); }); }; const registerConnection = ( container: IContainer, connection: Connection, ): Connection => { container.register(CONNECTION, connection); return connection; }; const connect = ( container: IContainer, config: IHadronTypeormConfig, ): Promise => { const { connection } = config; return createConnection(connection) .then((connection) => registerConnection(container, connection)) .then((connection: Connection) => { const entitiesToRegister = [ ...(config.connection.entities || []), ...(config.connection.entitySchemas || []), ]; registerRepositories(container, connection, entitiesToRegister); return connection; }) .catch((err) => { console.error(err); }); }; export { connect, registerRepositories }; ================================================ FILE: packages/hadron-typeorm/src/constants.ts ================================================ export const CONNECTION = 'connection'; ================================================ FILE: packages/hadron-typeorm/src/types.ts ================================================ import { ConnectionOptions } from 'typeorm'; export interface IHadronTypeormConfig { connection: ConnectionOptions | any; } ================================================ FILE: packages/hadron-typeorm/tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "target": "es6", "noImplicitAny": true, "noEmitOnError": true, "moduleResolution": "node", "sourceMap": true, "outDir": "dist", "baseUrl": ".", "paths": { "*": ["./*"] }, "emitDecoratorMetadata": true, "experimentalDecorators": true, "inlineSources": true, "lib": ["es2017", "dom"] }, "include": ["src/**/*.*", "index.ts", "LICENSE"], "exclude": ["node_modules", "**/__tests__/**", "**/*/*.d.ts"] } ================================================ FILE: packages/hadron-utils/LICENSE ================================================ MIT License Copyright (c) 2018 Brainhub Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/hadron-utils/README.md ================================================ # Utility tools for Hadron ================================================ FILE: packages/hadron-utils/index.ts ================================================ import getArgs from './src/getArgs'; export { getArgs }; ================================================ FILE: packages/hadron-utils/package.json ================================================ { "name": "@brainhubeu/hadron-utils", "version": "1.0.0", "description": "Set of utility functions for hadron", "main": "dist/index.js", "files": [ "dist", "./LICENSE" ], "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "prepare": "tsc" }, "keywords": [ "hadron", "utils", "brainhub" ], "author": "Brainhub", "license": "MIT", "publishConfig": { "access": "public" } } ================================================ FILE: packages/hadron-utils/src/__tests__/getArgs.ts ================================================ /* tslint:disable:max-classes-per-file */ import { expect } from 'chai'; import getArgs from '../getArgs'; describe('getArgs return list of arguments', () => { it("function declaration - should return ['bar', 'bar2']", () => { function foo(bar: any, bar2: any): any { return null; } const args = getArgs(foo); expect(['bar', 'bar2']).to.deep.equal(args); }); it("function expression - should return ['bar', 'bar2']", () => { const foo = (bar: any, bar2: any) => { const t = ''; return t; }; const args = getArgs(foo); expect(['bar', 'bar2']).to.deep.equal(args); }); it("class declaration - should return ['bar', 'bar2']", () => { class Foo { constructor(bar: any, bar2: any) { return null; } } const args = getArgs(Foo); expect(['bar', 'bar2']).to.deep.equal(args); }); it("class expression - should return ['bar', 'bar2']", () => { const foo = class { constructor(bar: any, bar2: any) { const t = ''; return t; } }; const args = getArgs(foo); expect(['bar', 'bar2']).to.deep.equal(args); }); it("object method is no constructable - should return ['bar', 'bar2']", () => { const foo = { foo(bar: any, bar2: any): any { return null; }, }.foo; const args = getArgs(foo); expect(['bar', 'bar2']).to.deep.equal(args); }); it("class method - should return ['bar', 'bar2']", () => { class Foo { public bar(bar: any, bar2: any): any { return null; } } const bar = new Foo().bar; const args = getArgs(bar); expect(['bar', 'bar2']).to.deep.equal(args); }); it("static class method - should return ['bar', 'bar2']", () => { class Foo { public static bar(bar: any, bar2: any): any { return null; } } const bar = Foo.bar; const args = getArgs(bar); expect(['bar', 'bar2']).to.deep.equal(args); }); }); ================================================ FILE: packages/hadron-utils/src/getArgs.ts ================================================ const getArgs = (f: (args: any) => any): string[] => { const STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/gm; const ARGUMENT_NAMES = /([^\s,]+)/g; const funcContent = f.toString().replace(STRIP_COMMENTS, ''); return ( funcContent .slice(funcContent.indexOf('(') + 1, funcContent.indexOf(')')) .match(ARGUMENT_NAMES) || [] ); }; export default getArgs; ================================================ FILE: packages/hadron-utils/tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "target": "es6", "noImplicitAny": true, "noEmitOnError": true, "moduleResolution": "node", "sourceMap": true, "outDir": "dist", "baseUrl": ".", "paths": { "*": ["./*"] }, "emitDecoratorMetadata": true, "experimentalDecorators": true, "inlineSources": true, "lib": ["es2017", "dom"] }, "include": ["src/**/*.*", "index.ts", "LICENSE"], "exclude": ["node_modules", "**/__tests__/**", "**/*/*.d.ts"] } ================================================ FILE: packages/hadron-validation/LICENSE ================================================ MIT License Copyright (c) 2018 Brainhub Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/hadron-validation/README.md ================================================ ## Installation ```bash npm install @brainhubeu/hadron-validation --save ``` [More info about installation](/core/#installation) ## Creating schema files To use validation layer, first you need to provide some schemas. We use JSON Schema format, for example: ```json { "type": "object", "properties": { "id": { "type": "number" }, "name": { "type": "string" }, "age": { "type": "number" } }, "required": ["name", "age"], "additionalProperties": false } ``` Full documentation about JSON Schema: [Ajv documentation](https://epoberezkin.github.io/ajv/) ## Preparing schema files for usage with Hadron When your schemas are ready you need to build object from them, where **key name** is a name of the schema, for example: ```js // schemas.js const insertTeam = require('./team/insertTeam.json'); const updateTeam = require('./team/updateTeam.json'); const insertUser = require('./user/insertUser.json'); const updateUser = require('./user/updateUser.json'); const schemas = { insertTeam, updateTeam, insertUser, updateUser, }; module.exports = schemas; ``` ## Validate function After you create schemas object you can create validate function from **Hadron validator factory** ```js // validate.js const validatorFactory = require('@brainhubeu/hadron-validation'); const schemas = require('./schemas'); module.exports = validatorFactory(schemas); ``` ## Using validate function After you created validate function with **hadron validator factory** you can use it to validate object by schemas you provide. ```js const validObject = { name: 'Max', age: 22, }; validate('schemaName', objectToValidate) .then((validObject) => { console.log('I am a valid object', validObject); }) .catch((error) => { console.log('Object is invalid', error); }); ``` Validate function passes valid object, otherwise it throws an error. ================================================ FILE: packages/hadron-validation/index.ts ================================================ import validatorFactory from './src/validator-factory'; export default validatorFactory; ================================================ FILE: packages/hadron-validation/package.json ================================================ { "name": "@brainhubeu/hadron-validation", "version": "1.0.0", "description": "Hadron validation layer module", "main": "dist/index.js", "files": [ "dist", "./LICENSE" ], "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "prepare": "tsc" }, "keywords": [ "hadron", "brainhub", "hadron-validation" ], "author": "Brainhub", "license": "MIT", "dependencies": { "ajv": "6.4.0" }, "publishConfig": { "access": "public" } } ================================================ FILE: packages/hadron-validation/src/__tests__/__mocks__/Team.ts ================================================ import { User } from './User'; export class Team { public id: number; public name: string; public users: User[]; } ================================================ FILE: packages/hadron-validation/src/__tests__/__mocks__/User.ts ================================================ import { Team } from './Team'; export class User { public id: number; public name: string; public team: Team; } ================================================ FILE: packages/hadron-validation/src/__tests__/__mocks__/declarations.d.ts ================================================ declare module '*.json' { const value: any; // @ts-ignore export default value; } ================================================ FILE: packages/hadron-validation/src/__tests__/__mocks__/test-schemas/email.json ================================================ { "type": "object", "properties": { "html": { "type": "string" }, "text": { "type": "string" } } } ================================================ FILE: packages/hadron-validation/src/__tests__/__mocks__/test-schemas/person.json ================================================ { "type": "object", "properties": { "firstname": { "type": "string" }, "lastname": { "type": "string" }, "age": { "type": "number" } }, "additionalProperties": false } ================================================ FILE: packages/hadron-validation/src/__tests__/__mocks__/test-schemas/team.json ================================================ { "type": "object", "properties": { "id": { "type": "number" }, "name": { "type": "string" }, "users": { "$ref": "users" } } } ================================================ FILE: packages/hadron-validation/src/__tests__/__mocks__/test-schemas/user.json ================================================ { "type": "object", "properties": { "id": { "type": "number" }, "name": { "type": "string" }, "team": { "$ref": "team" } } } ================================================ FILE: packages/hadron-validation/src/__tests__/__mocks__/test-schemas/users.json ================================================ { "type": "array", "description": "users in team", "items": { "$ref": "user" } } ================================================ FILE: packages/hadron-validation/src/__tests__/__mocks__/test-schemas.ts ================================================ import email = require('./test-schemas/email.json'); import person = require('./test-schemas/person.json'); import team = require('./test-schemas/team.json'); import user = require('./test-schemas/user.json'); import users = require('./test-schemas/users.json'); const schemas = { email, person, team, user, users, }; export default schemas; ================================================ FILE: packages/hadron-validation/src/__tests__/__mocks__/test-validate.ts ================================================ import schemas from './test-schemas'; import validatorFactory from '../../validator-factory'; export default validatorFactory(schemas); ================================================ FILE: packages/hadron-validation/src/__tests__/validate.ts ================================================ import { expect } from 'chai'; import { Team } from './__mocks__/Team'; import { User } from './__mocks__/User'; import validate from './__mocks__/test-validate'; describe('Validate', () => { it('should pass valid email', () => { const validEmail = { html: 'abc', text: 'abc', }; return validate('email', validEmail).then((data) => { expect(data).to.be.deep.equal(validEmail); }); }); it('should return error if email is invalid', () => { const invalidEmail = { html: 'abc', text: 1, }; return validate('email', invalidEmail).catch((error) => { expect(error).to.be.an.instanceof(Error); }); }); it('should pass valid user', () => { const team = new Team(); team.id = 1; team.name = 'Team One'; const user = new User(); user.id = 1; user.name = 'Max'; user.team = team; return validate('user', user).then((data) => { expect(data).to.be.deep.equal(user); }); }); it('should return error if user team is null', () => { const user = new User(); user.id = 1; user.name = 'Max'; user.team = null; return validate('user', user).catch((error) => { expect(error).to.be.an.instanceof(Error); }); }); it('should check if users in team are User objects', () => { const dummyArr = [null, new User()]; const team = new Team(); team.id = 1; team.name = 'Team'; team.users = dummyArr; return validate('team', team).catch((error) => { expect(error).to.be.an.instanceof(Error); }); }); }); ================================================ FILE: packages/hadron-validation/src/validator-factory.ts ================================================ import * as JsonSchemaValidator from 'ajv'; const factory = (schemas: any) => { const validator = new JsonSchemaValidator({ allErrors: true }); Object.keys(schemas).forEach((key) => { const schema = Object.assign( { $async: true, $schema: 'http://json-schema.org/draft-07/schema#', title: key, }, schemas[key], ); validator.addSchema(schema, key); }); return (name: string, dataToValidate: any) => { return new Promise((resolve, reject) => { const validation = validator.validate(name, dataToValidate); if (validation instanceof Promise) { validation .then(resolve) .catch((error) => reject( Error(`Error: ${error.message} ${JSON.stringify(error.errors)}`), ), ); } else { return validation === true ? resolve(dataToValidate) : reject(Error('Error: Validation failed.')); } }); }; }; export default factory; ================================================ FILE: packages/hadron-validation/tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "target": "es6", "noImplicitAny": true, "noEmitOnError": true, "moduleResolution": "node", "sourceMap": true, "outDir": "dist", "baseUrl": ".", "paths": { "*": ["./*"] }, "emitDecoratorMetadata": true, "experimentalDecorators": true, "inlineSources": true, "lib": ["es2017", "dom"] }, "include": ["src/**/*.*", "index.ts", "LICENSE"], "exclude": ["node_modules", "**/__tests__/**", "**/*/*.d.ts"] } ================================================ FILE: pm2.config.js ================================================ module.exports = { apps: [ { name: process.env.NAME, script: 'index.js', instances: Number(process.env.CONCURRENT || 1), exec_mode: 'cluster', log_type: 'json', }, ], }; ================================================ FILE: scripts/clean ================================================ lerna clean --yes for d in $PWD/packages/* ; do echo "Clearing dist in $d"; cd $d && rm -rf dist done ================================================ FILE: scripts/copy-tsconfig ================================================ for d in ./packages/* ; do echo "$d"; cp tsconfig.json ./$d/ done ================================================ FILE: test.sh ================================================ #!/bin/bash port="${PORT:-8080}" trap 'kill $!' EXIT PORT="$port" npm run start:test & tries=0 while ! echo exit 1 | nc localhost "$port"; do if ((tries >= 10)) then echo 'Cannot connect to test server!' exit 1; fi ((tries++)) sleep 5; done npm run test:cucumber ================================================ FILE: tools/testSetup.ts ================================================ import * as chai from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import * as dirtyChai from 'dirty-chai'; import * as sinonChai from 'sinon-chai'; chai.use(chaiAsPromised); chai.use(sinonChai); chai.use(dirtyChai); ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "target": "es6", "noImplicitAny": true, "noEmitOnError": true, "moduleResolution": "node", "sourceMap": true, "outDir": "dist", "baseUrl": ".", "paths": { "*": ["./*"] }, "emitDecoratorMetadata": true, "experimentalDecorators": true, "inlineSources": true, "lib": ["es2017", "dom"] }, "include": ["src/**/*.*", "index.ts", "LICENSE"], "exclude": ["node_modules", "**/__tests__/**", "**/*/*.d.ts"] } ================================================ FILE: tslint.json ================================================ { "extends": [ "tslint:recommended", "tslint-config-airbnb", "tslint-eslint-rules", "tslint-config-prettier" ], "rules": { "no-console": { "severity": "warning", "options": ["debug", "info", "log", "time", "timeEnd", "trace"] }, "no-conditional-assignment": true, "no-debugger": true, "switch-default": true, "forin": true, "no-arg": true, "no-empty": true, "no-invalid-this": false, "label-position": true, "no-multi-spaces": true, "no-unused-expression": true, "no-return-assign": false, "no-string-throw": true, "radix": true, "no-shadowed-variable": false, "no-unused-variable": [true, { "ignore-pattern": "^I" }], "no-use-before-declare": true, "variable-name": true, "one-variable-per-declaration": true, "object-literal-sort-keys": false, "ordered-imports": false, "comment-format": [true, "check-space"], "no-duplicate-imports": true, "no-var-keyword": true, "object-literal-shorthand": true, "prefer-const": true, "prefer-template": true, "import-name": false, "prefer-array-literal": false, "no-parameter-reassignment": false, "align": false }, "linterOptions": { "exclude": ["**/*.js"] } }