├── .babelrc ├── .circleci └── config.yml ├── .dockerignore ├── .editorconfig ├── .env.dev ├── .env.example ├── .env.localstack ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .prettierrc ├── .tool-versions ├── .vscode └── launch.json ├── README.md ├── TODO.md ├── commitlint.config.js ├── debug-lambda.sh ├── diagram.png ├── docker-compose.yml ├── docs ├── adapters.html ├── adapters_index.js.html ├── adapters_todo.js.html ├── adapters_todo.spec.js.html ├── business.html ├── business_constants.js.html ├── business_index.js.html ├── business_moment.js.html ├── business_todo.js.html ├── config.html ├── config_environments.js.html ├── config_index.js.html ├── fonts │ ├── OpenSans-Bold-webfont.eot │ ├── OpenSans-Bold-webfont.svg │ ├── OpenSans-Bold-webfont.woff │ ├── OpenSans-BoldItalic-webfont.eot │ ├── OpenSans-BoldItalic-webfont.svg │ ├── OpenSans-BoldItalic-webfont.woff │ ├── OpenSans-Italic-webfont.eot │ ├── OpenSans-Italic-webfont.svg │ ├── OpenSans-Italic-webfont.woff │ ├── OpenSans-Light-webfont.eot │ ├── OpenSans-Light-webfont.svg │ ├── OpenSans-Light-webfont.woff │ ├── OpenSans-LightItalic-webfont.eot │ ├── OpenSans-LightItalic-webfont.svg │ ├── OpenSans-LightItalic-webfont.woff │ ├── OpenSans-Regular-webfont.eot │ ├── OpenSans-Regular-webfont.svg │ └── OpenSans-Regular-webfont.woff ├── global.html ├── global.html#CustomError ├── index.html ├── ports_aws-lambda_todo.js.html ├── ports_http_routes_todo.router.js.html ├── scripts │ ├── linenumber.js │ └── prettify │ │ ├── Apache-License-2.0.txt │ │ ├── lang-css.js │ │ └── prettify.js ├── styles │ ├── jsdoc-default.css │ ├── prettify-jsdoc.css │ └── prettify-tomorrow.css ├── utils.html ├── utils_errors.js.html └── utils_index.js.html ├── hexagonal-architecture.png ├── iaac └── localstack │ ├── lambdas.tf │ ├── locals.tf │ ├── outputs.tf │ ├── provider.tf │ ├── state-machines.tf │ └── variables.tf ├── jest.config.js ├── jsdoc.conf.json ├── mocks └── lambda │ └── events │ └── create_todo.json ├── package.json ├── serverless.yaml ├── src ├── adapters │ ├── index.js │ ├── todo.js │ └── todo.spec.js ├── business │ ├── constants.js │ ├── constants.spec.js │ ├── index.js │ ├── moment.js │ ├── moment.spec.js │ ├── todo.js │ └── todo.spec.js ├── config │ ├── environments.js │ ├── index.js │ └── index.spec.js ├── ports │ ├── aws-lambda │ │ ├── index.js │ │ └── todo.js │ ├── http │ │ ├── bin │ │ │ └── www.js │ │ ├── controllers │ │ │ ├── index.js │ │ │ └── todo.controller.js │ │ ├── index.js │ │ └── routes │ │ │ ├── index.js │ │ │ ├── todo.router.js │ │ │ └── utils.js │ ├── logger │ │ ├── index.js │ │ └── logger.js │ └── state-machines │ │ ├── aws.dynamo.js │ │ ├── aws.dynamo.spec.js │ │ ├── aws.sqs.js │ │ ├── aws.sqs.spec.js │ │ ├── constants.js │ │ ├── constants.spec.js │ │ └── index.js └── utils │ ├── errors.js │ ├── errors.spec.js │ └── index.js ├── stryker.conf.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | node: circleci/node@2.0.1 4 | coveralls: coveralls/coveralls@1.0.4 5 | jobs: 6 | build-and-test: 7 | executor: 8 | name: node/default 9 | tag: "12.18.1" 10 | steps: 11 | - run: sudo apt-get update 12 | - run: sudo apt-get install -y tzdata 13 | - run: sudo ln -fs /usr/share/zoneinfo/America/Sao_Paulo /etc/localtime 14 | - checkout 15 | - run: yarn 16 | - run: yarn test-ci 17 | - coveralls/upload: 18 | path_to_lcov: "./reports/coverage/lcov.info" 19 | - store_test_results: 20 | path: test-results 21 | build-and-test-and-send-to-stryker: 22 | executor: 23 | name: node/default 24 | tag: "12.18.1" 25 | steps: 26 | - run: sudo apt-get update 27 | - run: sudo apt-get install -y tzdata 28 | - run: sudo ln -fs /usr/share/zoneinfo/America/Sao_Paulo /etc/localtime 29 | - checkout 30 | - run: yarn 31 | - run: yarn test-ci 32 | - run: 33 | name: Stryker mutator 34 | command: yarn stryker-mutate --reporters dashboard --dashboard.version $CIRCLE_BRANCH 35 | - coveralls/upload: 36 | path_to_lcov: "./reports/coverage/lcov.info" 37 | - store_test_results: 38 | path: test-results 39 | workflows: 40 | build-and-test: 41 | jobs: 42 | - build-and-test: 43 | filters: 44 | branches: 45 | ignore: /^master/ 46 | - build-and-test-and-send-to-stryker: 47 | filters: 48 | branches: 49 | ignore: /^(?!master).*$/ 50 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .circleci 2 | .github 3 | .vscode 4 | node_modules 5 | dist 6 | logs 7 | *.log 8 | .env* 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | trim_trailing_spaces = true 8 | insert_final_newline = true 9 | charset = utf-8 10 | 11 | [*.js] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [{package.json,*.yml,*.cjson}] 16 | indent_style = space 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /.env.dev: -------------------------------------------------------------------------------- 1 | # this file is exclusive for serverless offline development 2 | ################################### 3 | # GLOBAL AND DEBUG ENVS 4 | ################################### 5 | APP_NAME='hexagonal-boilerplate' 6 | ENV_NAME='development ' 7 | TIMEZONE='America/Sao_Paulo' 8 | ################################### 9 | # AWS SERVICES 10 | ################################### 11 | # DYNAMO 12 | AWS_DYNAMO_REGION='us-east-1' 13 | AWS_DYNAMO_APIVERSION='2012-08-10' 14 | AWS_DYNAMO_TODO_TABLE_NAME='todos' 15 | # SQS 16 | AWS_SQS_REGION='us-east-1' 17 | AWS_SQS_APIVERSION='2012-11-05' 18 | # S3 19 | AWS_S3_APIVERSION='2006-03-01' 20 | ################################### 21 | # LOCALSTACK 22 | ################################### 23 | PROJECT_NAME=hexagonal_boilerplate 24 | LAMBDA_EXECUTOR=docker 25 | LAMBDA_DOCKER_NETWORK=$PROJECT_NAME 26 | AWS_REGION='us-east-1' 27 | AWS_ACCESS_KEY_ID='xxxx' 28 | AWS_ACCESS_SECRET_KEY='xxxx' 29 | LOCALSTACK_SERVICES=s3,sqs,dynamodb,lambda,cloudformation,sts,iam 30 | AWS_DYNAMO_ENDPOINT='http://localhost:4566' 31 | AWS_SQS_REPORT_QUEUE_URL='http://localhost:4566/queue/report-queue' 32 | AWS_SQS_REPORT_QUEUE_URL_ERROR='http://localhost:4566/queue/report-queue-error' 33 | AWS_S3_URL='http://localhost:4566' 34 | # AWS_DYNAMODB_STREAM_URL='http://localhost:4570' 35 | DATA_DIR=/tmp/localstack/data 36 | DEBUG=1 37 | LAMBDA_REMOTE_DOCKER=0 38 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | ################################### 2 | # GLOBAL AND DEBUG ENVS 3 | ################################### 4 | APP_NAME='hexagonal-boilerplate' 5 | ENV_NAME='development' 6 | TIMEZONE='America/Sao_Paulo' 7 | 8 | ################################### 9 | # AWS SERVICES 10 | ################################### 11 | # DYNAMO 12 | AWS_DYNAMO_REGION='us-east-1' 13 | AWS_DYNAMO_APIVERSION='2012-08-10' 14 | AWS_DYNAMO_TODO_TABLE_NAME='todo' 15 | # SQS 16 | AWS_SQS_REGION='us-east-1' 17 | AWS_SQS_APIVERSION='2012-11-05' 18 | # S3 19 | AWS_S3_APIVERSION='2006-03-01' 20 | ################################### 21 | # LOCALSTACK 22 | ################################### 23 | AWS_REGION='us-east-1' 24 | AWS_ACCESS_KEY_ID='xxxx' 25 | AWS_ACCESS_SECRET_KEY='xxxx' 26 | LOCALSTACK_SERVICES=s3,sqs,dynamodb 27 | AWS_DYNAMO_ENDPOINT='http://localhost:8000' 28 | AWS_SQS_TODO_QUEUE_URL='http://localhost:8000' 29 | -------------------------------------------------------------------------------- /.env.localstack: -------------------------------------------------------------------------------- 1 | # this file is exclusive for serverless localstack development 2 | ################################### 3 | # GLOBAL AND DEBUG ENVS 4 | ################################### 5 | APP_NAME='hexagonal-boilerplate' 6 | ENV_NAME='development' 7 | TIMEZONE='America/Sao_Paulo' 8 | 9 | ################################### 10 | # AWS SERVICES 11 | ################################### 12 | # DYNAMO 13 | AWS_DYNAMO_REGION='us-east-1' 14 | AWS_DYNAMO_APIVERSION='2012-08-10' 15 | AWS_DYNAMO_TODO_TABLE_NAME='todos' 16 | # SQS 17 | AWS_SQS_REGION='us-east-1' 18 | AWS_SQS_APIVERSION='2012-11-05' 19 | # S3 20 | AWS_S3_APIVERSION='2006-03-01' 21 | ################################### 22 | # LOCALSTACK 23 | ################################### 24 | PROJECT_NAME=hexagonal_boilerplate 25 | LAMBDA_EXECUTOR=docker 26 | LAMBDA_DOCKER_NETWORK=$PROJECT_NAME 27 | AWS_REGION='us-east-1' 28 | AWS_ACCESS_KEY_ID='xxxx' 29 | AWS_ACCESS_SECRET_KEY='xxxx' 30 | LOCALSTACK_SERVICES=s3,sqs,dynamodb,lambda,cloudformation,sts,iam 31 | AWS_DYNAMO_ENDPOINT='http://localstack_hexagonal_boilerplate:4566' 32 | AWS_SQS_REPORT_QUEUE_URL='http://localstack_hexagonal_boilerplate:4566/queue/report-queue' 33 | AWS_S3_URL='http://localstack_hexagonal_boilerplate:4566' 34 | # AWS_DYNAMODB_STREAM_URL='http://localstack_hexagonal_boilerplate:4570' 35 | DATA_DIR=/tmp/localstack/data 36 | DEBUG=1 37 | LAMBDA_REMOTE_DOCKER=0 38 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | commitlint.config.js 4 | jest.config.js 5 | stryker.conf.js 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "standard" 5 | ], 6 | "rules": { 7 | "space-before-function-paren": "off", 8 | "immutable/no-let": 2, 9 | "immutable/no-this": 2, 10 | "immutable/no-mutation": 2, 11 | // "jsdoc/check-alignment": 1, // Recommended 12 | // "jsdoc/check-examples": 0, 13 | // "jsdoc/check-indentation": 1, 14 | // "jsdoc/check-param-names": 1, // Recommended 15 | // "jsdoc/check-syntax": 1, 16 | // "jsdoc/check-tag-names": 1, // Recommended 17 | // "jsdoc/check-types": 1, // Recommended 18 | // "jsdoc/implements-on-classes": 1, // Recommended 19 | // "jsdoc/match-description": 1, 20 | // "jsdoc/newline-after-description": 1, // Recommended 21 | // "jsdoc/no-types": 1, 22 | // "jsdoc/no-undefined-types": 1, // Recommended 23 | // "jsdoc/require-description": 1, 24 | // "jsdoc/require-description-complete-sentence": 1, 25 | // "jsdoc/require-example": 1, 26 | // "jsdoc/require-hyphen-before-param-description": 1, 27 | // "jsdoc/require-jsdoc": 1, // Recommended 28 | // "jsdoc/require-param": 1, // Recommended 29 | // "jsdoc/require-param-description": 1, // Recommended 30 | // "jsdoc/require-param-name": 1, // Recommended 31 | // "jsdoc/require-param-type": 1, // Recommended 32 | // "jsdoc/require-returns": 1, // Recommended 33 | // "jsdoc/require-returns-check": 1, // Recommended 34 | // "jsdoc/require-returns-description": 1, // Recommended 35 | // "jsdoc/require-returns-type": 1, // Recommended 36 | // "jsdoc/valid-types": 0 // Recommended 37 | "jest/no-disabled-tests": "warn", 38 | "jest/no-focused-tests": "error", 39 | "jest/no-identical-title": "error", 40 | "jest/prefer-to-have-length": "warn", 41 | "jest/valid-expect": "error" 42 | }, 43 | "plugins": [ 44 | "fp", 45 | "immutable", 46 | "jest" 47 | ], 48 | "env": { 49 | "jest/globals": true, 50 | "jest": true, 51 | "browser": true 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | .env 5 | yarn-error.log 6 | .vscode/settings.json 7 | .serverless 8 | .localstack 9 | **/*/.terraform 10 | **/*/terraform.tfstate 11 | **/*/terraform.tfstate.backup 12 | test-results 13 | .scannerwork/ 14 | reports/ 15 | terraform.tfstate 16 | terraform.tfstate.backup 17 | .terraform/ 18 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | tabWidth: 2 2 | semi: false 3 | singleQuote: true 4 | trailingComma: "none" 5 | bracketSpacing: true 6 | arrowParens: "avoid" 7 | endOfLine: "lf" 8 | jsxSingleQuote: true 9 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 12.18.1 2 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "outFiles": [ 15 | "/**" 16 | ], 17 | "program": "${workspaceFolder}/dist/ports/http/bin/www.js" 18 | }, 19 | { 20 | "type": "node", 21 | "request": "launch", 22 | "name": "Jest Test", 23 | "program": "${workspaceFolder}/node_modules/.bin/jest", 24 | "args": [ 25 | "--runInBand", 26 | "--config=${workspaceFolder}/jest.config.js", 27 | "--detectOpenHandles" 28 | ], 29 | "internalConsoleOptions": "neverOpen" 30 | }, 31 | { 32 | "type": "node", 33 | "request": "launch", 34 | "name": "run on lambda port", 35 | "skipFiles": [ 36 | "/**" 37 | ], 38 | "outFiles": [ 39 | "/**" 40 | ], 41 | "program": "${workspaceFolder}/node_modules/.bin/serverless", 42 | "args": [ 43 | "offline", 44 | "--env", 45 | "dev" 46 | ] 47 | } 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Boilerplate de Arquitetura Hexagonal 2 | 3 | [![Coverage Status](https://coveralls.io/repos/github/claytonsilva/nodejs-hexagonal-boilerplate/badge.svg?branch=master)](https://coveralls.io/github/claytonsilva/nodejs-hexagonal-boilerplate?branch=master) 4 | [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fclaytonsilva%2Fnodejs-hexagonal-boilerplate%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/claytonsilva/nodejs-hexagonal-boilerplate/master) 5 | [![Standard - JavaScript Style Guide](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) 6 | [![Conventional Changelog](https://img.shields.io/badge/changelog-conventional-brightgreen.svg)](http://conventional-changelog.github.io) 7 | [![Standard Version](https://img.shields.io/badge/release-standard%20version-brightgreen.svg)](https://github.com/conventional-changelog/standard-version) 8 | [![CircleCI](https://circleci.com/gh/claytonsilva/nodejs-hexagonal-boilerplate.svg?style=svg)](https://circleci.com/gh/claytonsilva/nodejs-hexagonal-boilerplate) 9 | 10 | ## Inspiração 11 | 12 | Inspirado em desenvolver essa arquitetura depois de acompanhar algumas palestras da Nubank, a ideia de separação de responsabilidades por camadas e modelos de testes me fez inspirar a criar um modelo em Node.js. 13 | 14 | ## O que vem a ser? Aonde vive? Hoje no globo reporter 15 | 16 | Antes de tudo vamos no [Wikipedia](https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)) 17 | Depois vamos paginar [esse slide bacana da Nubank](https://pt.slideshare.net/Nubank/arquitetura-funcional-em-microservices). 18 | 19 | ## Já viu os dois? senta que lá vem história 20 | 21 | ![arquitetura](./hexagonal-architecture.png) 22 | 23 | Vamos elencar algumas dores do desenvolvimento: 24 | 25 | * Mockar serviços ou testar neles; 26 | * Criar uma condição de teste com volume aceitável de cobertura, e possibilidade do seu teste evoluir 27 | conforme vai a experiência de consumo do produto; 28 | * Projetar Tolerância a falha; 29 | * Saber onde deve entrar o BDD e onde entra o teste unitário; e 30 | * (para galera que curte o lado funcional do Javascript) criar codigo 100% puro sem ter que vender o rim. 31 | 32 | ### Dado isso vamos começar pela camada do meio, aonde ficam os negócios 33 | 34 | * É a camada mais pura; 35 | * Não conversa com ninguém, somente é consumido; 36 | * Onde as funções DEVEM ser mais puras possíveis; 37 | * Não precisam de implementar injeção de dependência; 38 | * Não são assíncronas pois recebe tudo que precisa na entrada e devolve o objeto necessário; e 39 | * São fáceis de fazer teste unitário porque são puros e com entradas que se limita a Arrays e objetos. 40 | 41 | Nela deve ficar toda questão de negócios que sua solução propõe, sabemos que nessa área é onde vai ocorrer 42 | mais mudanças conforme for evoluindo sua aplicação, então ela tem que ser simpática e amigável a ponto de voce nem ligar 43 | que o jest rode na trigger de pos-commit. 44 | 45 | Concentre nela os casos de uso, nela que será construído o seu negócio. 46 | 47 | ### Adapters 48 | 49 | É a cola que une as camadas externas com negócio (é você controller?). 50 | 51 | Diferentemente do controller que foi projetado para arquitetura MVC e todo mundo já deixou alguma regra de negócio nele que eu sei e não adianta mentir, ele abstrai totalmente a ideia de ponte e pode ser aplicado em **qualquer contexto** dando uma flexibilidade grande para reaproveitamento de código. 52 | 53 | --- 54 | 55 | Importante ressaltar 56 | 57 | * camada de negócios conversa com adapter; 58 | * adapter conversa com ports; e 59 | * ~~camada de negócios fala com ports~~. 60 | 61 | --- 62 | 63 | O controller tinha responsabilidade de fazer ponte com a camada de modelo e ainda sanitizar os dados, preocupação que veremos na frente em **ports**. 64 | 65 | Aqui já ocorre consumo de serviços que precisam ser simulados (mock, emulador de serviços), então por consequência ocorre também injeção de dependência, para que a solução permita entrar com mock com facilidade sem alterar o contexto da função. O teste unitário começa a ficar mais complicado e começa os testes de comportamento, pois no adapter você está claramente consumindo o serviço, mas consumindo de forma direta. 66 | 67 | ### Ports 68 | 69 | As bordas que dão a fama de arquitetura hexagonal, pois foi feito pra abstrair como um hexágono onde cada lado significa uma porta I/O. 70 | 71 | Exemplos de ports: 72 | 73 | * máquinas de estado (fila, banco de dados); 74 | * handlers em lambda; 75 | * serviço http com express; e 76 | * log shipper. 77 | 78 | Ele pode ser conectado ao ambiente real ou simulado, onde também ocorre injeção de dependência e o contexto da avaliação de comportamento foge do contexto das regras de negócio. 79 | 80 | Começa a ficar convidativo fazer ainda o BDD, mas com as portas podemos ir além, através de simulação de serviços como [localstack](https://localstack.cloud/) podemos chegar a simular alguns volumes de carga (não generosos por ser simulado e não ter o mesmo throughput de um ambiente real), e usar [Chaos Monkey](https://en.wikipedia.org/wiki/Chaos_engineering), isso é possível porque o localstack permite que você simule taxas de erros nos serviços para testes de resiliência. 81 | 82 | Esse [projeto da Netflix](https://github.com/Netflix/chaosmonkey) pode ampliar seus serviços pra teste como banco de dados por exemplo. 83 | 84 | Na integração contínua o localstack é amigável para manter o mesmo ambiente em várias fases até chegar em produção passando por várias situações semelhante a serviços reais. 85 | 86 | ![diagrama](./diagram.png) 87 | 88 | ### Como usar esse projeto 89 | 90 | É bem simples de já começar rodando 91 | 92 | 1. Configure o `.env` baseado no `.env.example`; 93 | 2. ligue o localstack com `docker-compose up -d`; 94 | 3. levante o ambiente demo usando [terraform](https://www.terraform.io/) com os comandos: 95 | ```bash 96 | $terraform init 97 | $terraform plan (avalia se é isso mesmo que quer criar) 98 | $terraform apply 99 | ``` 100 | 4. instale as dependências com `yarn install`; e 101 | 5. rode o projeto com yarn start. 102 | 103 | Irá levantar uma instância de serviço http(Express), e seja feliz no consumo 104 | 105 | ## Features desse boilerplate 106 | 107 | 1. JsDoc implementado em todos os métodos e separados em namespaces na documentação que é gerado toda vez que aplica commit; 108 | 2. localstack como ambiente de simulação; e 109 | 3. Javascript ECMA 2015 com linters que te ajudam a manter o código funcional e puro. 110 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | * ~~setup lambda environment for test with this [link](https://gist.github.com/crypticmind/c75db15fd774fe8f53282c3ccbe3d7ad)~~ 4 | 5 | * ~~create tests~~ 6 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'type-enum': [2, 'always', [ 5 | 'feat', 6 | 'fix', 7 | 'docs', 8 | 'style', 9 | 'refactor', 10 | 'perf', 11 | 'test', 12 | 'chore', 13 | 'revert', 14 | 'ci', 15 | 'build' 16 | ]], 17 | 'header-max-length': [2, 'always', 100] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /debug-lambda.sh: -------------------------------------------------------------------------------- 1 | if [[ "$OSTYPE" == "linux-gnu"* ]]; then 2 | aws lambda invoke /dev/null --endpoint-url=http://localhost:3002 --function-name todo --payload $(cat mocks/lambda/events/create_todo.json | base64 -w 0) 3 | elif [[ "$OSTYPE" == "darwin"* ]]; then 4 | aws lambda invoke /dev/null --endpoint-url=http://localhost:3002 --function-name todo --payload $(cat mocks/lambda/events/create_todo.json | base64 -w 0) 5 | else 6 | echo "༼つಠ益ಠ༽つ ─=≡ΣO)) NOT NOW" 7 | fi 8 | -------------------------------------------------------------------------------- /diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/claytonsilva/nodejs-hexagonal-boilerplate/5f4df2cbf6a5e05a14c54fce88244f7f8d52b9b9/diagram.png -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | socat: 4 | image: bpack/socat 5 | command: TCP4-LISTEN:2375,fork,reuseaddr UNIX-CONNECT:/var/run/docker.sock 6 | networks: 7 | - inner 8 | volumes: 9 | - /var/run/docker.sock:/var/run/docker.sock 10 | expose: 11 | - "2375" 12 | env_file: .env 13 | dynamo-admin: 14 | image: instructure/dynamo-local-admin 15 | container_name: "dynamoadmin_${PROJECT_NAME:-local_project}" 16 | networks: 17 | - inner 18 | ports: 19 | - "8000:8000" 20 | environment: 21 | - DYNAMO_ENDPOINT=http://localstack:4566 22 | links: 23 | - localstack 24 | localstack: 25 | image: localstack/localstack:0.11.2 26 | container_name: "localstack_${PROJECT_NAME:-local_project}" 27 | networks: 28 | - inner 29 | env_file: .env 30 | ports: 31 | - "4566:4566" 32 | - "${PORT_WEB_UI-8080}:${PORT_WEB_UI-8080}" 33 | environment: 34 | - LOCALSTACK_SERVICES=${LOCALSTACK_SERVICES- } 35 | - LAMBDA_EXECUTOR=${LAMBDA_EXECUTOR-docker } 36 | - DEBUG=${DEBUG- } 37 | - DATA_DIR=${DATA_DIR- } 38 | - PORT_WEB_UI=${PORT_WEB_UI- } 39 | - DOCKER_HOST=tcp://socat:2375 40 | - HOST_TMP_FOLDER=${PWD} 41 | volumes: 42 | - ${PWD}/.localstack/tmp/localstack:/tmp/localstack 43 | links: 44 | - socat 45 | networks: 46 | inner: 47 | name: hexagonal_boilerplate 48 | driver: bridge 49 | -------------------------------------------------------------------------------- /docs/adapters_index.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: adapters/index.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: adapters/index.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
/**
30 |  * Adapters  Namespace.
31 |  * @namespace adapters
32 |  *
33 |  *
34 |  * @description this namespace control communication between business and state-machines
35 |  */
36 | 
37 | /**
38 |  * @typedef {Object} Adapter
39 |  * @property {TodoAdapter} todo todo adapter instantied
40 |  */
41 | 
42 | // eslint-disable-next-line no-unused-vars
43 | import { DynamoRepositoryInstance } from '../ports/state-machines'
44 | // code imports
45 | import todoAdapterFactory,
46 | // eslint-disable-next-line no-unused-vars
47 | { TodoAdapter } from './todo'
48 | 
49 | /**
50 |  * @description dynamo repository for state machine
51 |  *
52 |  * @memberof ports/state-machines
53 |  * @function
54 |  * @param {Logger} escriba - Instance of escriba.
55 |  * @param {DynamoRepositoryInstance} repository repository instatiated
56 |  * @returns {Adapter}
57 |  */
58 | export const adapter = (escriba, repository) => {
59 |   return {
60 |     todo: todoAdapterFactory(escriba, repository)
61 |   }
62 | }
63 | 
64 |
65 |
66 | 67 | 68 | 69 | 70 |
71 | 72 | 75 | 76 |
77 | 78 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /docs/adapters_todo.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: adapters/todo.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: adapters/todo.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
/**
 30 |  * Reference only imports (for documentation)
 31 | */
 32 | 
 33 | // eslint-disable-next-line no-unused-vars
 34 | import { Logger } from 'log4js'
 35 | // eslint-disable-next-line no-unused-vars
 36 | import { DynamoRepositoryInstance } from '../ports/state-machines'
 37 | // eslint-disable-next-line no-unused-vars
 38 | import { MutateTodoInput, Todo, TodoKey } from '../business'
 39 | 
 40 | /**
 41 |  * code imports
 42 |  */
 43 | 
 44 | import {
 45 |   // eslint-disable-next-line no-unused-vars
 46 |   CustomError,
 47 |   EClassError,
 48 |   throwCustomError
 49 | } from '../utils'
 50 | 
 51 | import { validateUpdateTodo, validateCreateTodo, validateDeleteTodo } from '../business/todo'
 52 | 
 53 | /**
 54 |  * @description Todo adapter factory
 55 |  * @memberof adapters
 56 |  * @function
 57 |  * @param {Logger} escriba instance of escriba logger
 58 |  * @param {DynamoRepositoryInstance} repository state-machine database methods
 59 |  * @returns {TodoAdapter} todo adapter instantied
 60 |  */
 61 | const todoAdapterFactory = (escriba, repository) => ({
 62 |   getTodo: getTodo(repository),
 63 |   createTodo: createTodo(escriba, repository),
 64 |   updateTodo: updateTodo(escriba, repository),
 65 |   deleteTodo: deleteTodo(escriba, repository)
 66 | })
 67 | 
 68 | export default todoAdapterFactory
 69 | 
 70 | /**
 71 |  * @description Handler function to get todo data by id .
 72 |  * @memberof adapters
 73 |  * @async
 74 |  * @function
 75 |  * @throws {CustomError}
 76 |  * @param {DynamoRepositoryInstance} repository - State-machine database methods.
 77 |  * @returns {getTodoReturn} GetDocument method ready to execute.
 78 |  */
 79 | const getTodo = (repository) => async (id) => {
 80 |   const methodPath = 'adapters.todo.getTodo'
 81 |   try {
 82 |     return await repository.getDocument({ id })
 83 |   } catch (error) {
 84 |     throwCustomError(error, methodPath, EClassError.INTERNAL)
 85 |   }
 86 | }
 87 | 
 88 | /**
 89 |  * @description Create todo in the DynamoDB.
 90 |  * @memberof adapters
 91 |  * @async
 92 |  * @function
 93 |  * @throws {CustomError}
 94 |  * @param {Logger} escriba instance of escriba
 95 |  * @param {DynamoRepositoryInstance} repository state-machine database methods
 96 |  * @returns {createTodoReturn} function to call createTodo direct
 97 |  */
 98 | const createTodo = (escriba, repository) => async (params, user) => {
 99 |   const methodPath = 'adapters.todo.createTodo'
100 |   try {
101 |     const documentInserted = await repository
102 |       .putDocument(
103 |         validateCreateTodo(
104 |           params,
105 |           user
106 |         )
107 |       )
108 | 
109 |     escriba.info({
110 |       action: 'TASK_CREATED',
111 |       method: methodPath,
112 |       data: { documentInserted }
113 |     })
114 | 
115 |     return documentInserted
116 |   } catch (error) {
117 |     throwCustomError(error, methodPath, EClassError.INTERNAL)
118 |   }
119 | }
120 | 
121 | /**
122 |  * @description Update todo in the DynamoDB.
123 |  * @memberof adapters
124 |  * @async
125 |  * @function
126 |  * @throws {CustomError}
127 |  * @param {Logger} escriba instance of escriba
128 |  * @param {DynamoRepositoryInstance} repository state-machine database methods
129 |  * @returns {updateTodoReturn} function to call updateTodo direct
130 |  */
131 | const updateTodo = (escriba, repository) => async (id, params, user) => {
132 |   const methodPath = 'adapters.todo.updateTodo'
133 |   try {
134 |     const currObject = await getTodo(repository)(id)
135 | 
136 |     const ExpressionAttributeValues = validateUpdateTodo(params, currObject, user)
137 | 
138 |     const UpdateExpression = `
139 |     set taskOrder = :taskOrder,
140 |         taskDescription = :taskDescription,
141 |         taskStatus = :taskStatus,
142 |         taskPriority = :taskPriority,
143 |         lastUpdateDate = :lastUpdateDate
144 |     `
145 |     // send report to existing todo previous created
146 |     const task = await repository.updateDocument(
147 |       { id },
148 |       UpdateExpression,
149 |       ExpressionAttributeValues
150 |     )
151 | 
152 |     // log report data
153 |     escriba.info({
154 |       action: 'TASK_UPDATED',
155 |       method: methodPath,
156 |       data: task
157 |     })
158 | 
159 |     // return updated item
160 |     return task
161 |   } catch (error) {
162 |     throwCustomError(error, methodPath, EClassError.INTERNAL)
163 |   }
164 | }
165 | 
166 | /**
167 |  * @description delete todo in the DynamoDB.
168 |  * @memberof adapters
169 |  * @async
170 |  * @function
171 |  * @throws {CustomError}
172 |  * @param {Logger} escriba instance of escriba
173 |  * @param {DynamoRepositoryInstance} repository state-machine database methods
174 |  * @returns {deleteTodoReturn} function to call deleteTodo direct
175 |  */
176 | const deleteTodo = (escriba, repository) => async (id, user) => {
177 |   const methodPath = 'adapters.todo.deleteTodo'
178 |   try {
179 |     const currObject = validateDeleteTodo(await getTodo(repository)(id), user)
180 |     await repository.deleteDocument({ id })
181 | 
182 |     // log report data
183 |     escriba.info({
184 |       action: 'TASK_DELETED',
185 |       method: methodPath,
186 |       data: currObject
187 |     })
188 | 
189 |     return currObject
190 |   } catch (error) {
191 |     throwCustomError(error, methodPath, EClassError.INTERNAL)
192 |   }
193 | }
194 | 
195 | /**
196 |  * complex callbacks documentation
197 |  *
198 |  */
199 | 
200 | /**
201 |  * @typedef {Object} TodoAdapter
202 |  * @property {getTodoReturn} getTodo function to get task by id (instantied)
203 |  * @property {createTodoReturn} createTodo function to generate task (instantiated).
204 |  * @property {updateTodoReturn} updateTodo function to update task  (instantiated).
205 |  * @property {deleteTodoReturn} deleteTodo function to delete task (instantiated).
206 |  */
207 | 
208 | /**
209 |  * This callback is displayed as part of the createTodo function.
210 |  * @memberof adapters
211 |  * @callback createTodoReturn
212 |  * @param {MutateTodoInput} params input param for createTodo
213 |  * @param {string} owner of the data entry logged
214 |  * @returns {Promise<Todo>} new report data
215 |  */
216 | 
217 | /**
218 |  * This callback is displayed as part of the updateTodo function.
219 |  * @memberof adapters
220 |  * @callback updateTodoReturn
221 |  * @param {string} id id of the current data for update
222 |  * @param {MutateTodoInput} params input param for updateTodo
223 |  * @param {string} owner of the data entry logged
224 |  * @returns {Promise<Todo>} new report data
225 |  */
226 | 
227 | /**
228 |  * This callback is displayed as part of the deleteTodo function.
229 |  * @memberof adapters
230 |  * @callback deleteTodoReturn
231 |  * @param {string} id id of the current data for update
232 |  * @param {string} owner of the data entry logged
233 |  * @returns {Promise<Todo>} new report data
234 |  */
235 | 
236 | /**
237 |  * This callback is displayed as part of the getTodo function.
238 |  * @memberof adapters
239 |  * @callback getTodoReturn
240 |  * @param {string} id key of the data
241 |  * @returns {Promise<Todo>} task from repository
242 |  */
243 | 
244 |
245 |
246 | 247 | 248 | 249 | 250 |
251 | 252 | 255 | 256 |
257 | 258 | 261 | 262 | 263 | 264 | 265 | 266 | -------------------------------------------------------------------------------- /docs/business_constants.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: business/constants.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: business/constants.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
/**
30 |  * Enum for ETodoStatus values.
31 |  * @readonly
32 |  * @memberof business
33 |  * @enum {string}
34 |  */
35 | export const ETodoStatus = {
36 |   NEW: 'NEW',
37 |   IN_PROGRESS: 'IN_PROGRESS',
38 |   WAITING_TRANSMISSION: 'WAITING_TRANSMISSION',
39 |   CLOSED: 'CLOSED',
40 |   CANCELED: 'CANCELED'
41 | }
42 | 
43 | /**
44 |  * Enum for EPriority values.
45 |  * @readonly
46 |  * @memberof business
47 |  * @enum {string}
48 |  */
49 | export const EPriority = {
50 |   LOW: 'LOW',
51 |   MODERATE: 'MODERATE',
52 |   HIGH: 'HIGH',
53 |   URGENT: 'URGENT'
54 | }
55 | 
56 |
57 |
58 | 59 | 60 | 61 | 62 |
63 | 64 | 67 | 68 |
69 | 70 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /docs/business_index.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: business/index.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: business/index.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
/**
30 |  * Business  Namespace.
31 |  * @namespace business
32 |  *
33 |  *
34 |  * @description this namespace control all business control of the solution
35 |  */
36 | /**
37 |  * code imports
38 |  */
39 | // eslint-disable-next-line no-unused-vars
40 | import { ETodoStatus, EPriority } from './constants'
41 | 
42 | /**
43 |  * @typedef {Object} TodoKey
44 |  * @property {string} id  id of the task
45 |  */
46 | 
47 | /**
48 | * @typedef {Object} Todo
49 | * @property {string} id  id of the task
50 | * @property {number} taskOrder  order of activity
51 | * @property {string} taskDescription description of taks
52 | * @property {user} taskOwner of the task
53 | * @property {ETodoStatus} taskStatus status of report
54 | * @property {EPriority} taskPriority priority of report
55 | * @property {string} creationDate datetime of creation
56 | * @property {string} lastUpdateDate datetime of the last update
57 | */
58 | 
59 | /**
60 | * @typedef {Object} MutateTodoInput  object to input in mutations
61 | * @property {number} taskOrder  order of activity
62 | * @property {string} taskDescription description of taks
63 | * @property {ETodoStatus} taskStatus status of report
64 | * @property {EPriority} taskPriority priority of report
65 | */
66 | 
67 | /**
68 | * @typedef {Object} MutateTodoOutput  object to input in mutations
69 | * @property {string} taskDescription description of taks
70 | * @property {ETodoStatus} taskStatus status of report
71 | * @property {EPriority} taskPriority priority of report
72 | * @property {string} lastUpdateDate datetime of the last update
73 | */
74 | 
75 |
76 |
77 | 78 | 79 | 80 | 81 |
82 | 83 | 86 | 87 |
88 | 89 | 92 | 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /docs/business_moment.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: business/moment.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: business/moment.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
// eslint-disable-next-line no-unused-vars
 30 | import { Moment, MomentInput } from 'moment'
 31 | // Code imports
 32 | import R from 'ramda'
 33 | import { momentConfig } from '../config'
 34 | import moment from 'moment-timezone/builds/moment-timezone-with-data'
 35 | 
 36 | import {
 37 |   EClassError,
 38 |   throwCustomError,
 39 |   // eslint-disable-next-line no-unused-vars
 40 |   CustomError
 41 | } from '../utils'
 42 | 
 43 | /**
 44 |  * @description Moment with timezone local
 45 |  * @memberof business
 46 |  * @function
 47 |  * @throws {CustomError}
 48 |  * @param  {MomentInput} dta (optional) override dta if necessary
 49 |  * @param  {string} timezone (optional) overload default timezone if necessary
 50 |  * @returns {Moment} moment with timezone configure
 51 |  */
 52 | const momentWithTz = (dta, timezone = momentConfig.timezone) => {
 53 |   if (!isValidEntry(dta)) {
 54 |     throwCustomError(new Error(`invalid dateTime entry, got "${dta}"`), 'business.moment.momentWithTz', EClassError.INTERNAL)
 55 |   }
 56 |   return (R.isNil(dta) ? moment() : moment(dta)).tz(timezone)
 57 | }
 58 | 
 59 | /**
 60 |  * @description Get the current time formated with 'YYYYMMDDHHmm'
 61 |  * @memberof business
 62 |  * @function
 63 |  * @param  {Moment} dta instantiate moment object
 64 |  * @returns {string} String datetime with format.
 65 |  */
 66 | const getDateFormated = (dta) => {
 67 |   return dta.format('YYYYMMDDHHmm')
 68 | }
 69 | 
 70 | /**
 71 |  * @description Moment with timezone local in iso8601
 72 |  * @memberof business
 73 |  * @function
 74 |  * @param  {Moment} dta (optional) moment instance for overload "new moment()" if necessary
 75 |  * @param  {string} timezone (optional) overload default timezone if necessary
 76 |  * @returns {string} iso8601 string datetime with timezone defined
 77 |  */
 78 | const toISOString = (dta, timezone = momentConfig.timezone) => {
 79 |   return (R.isNil(dta) ? momentWithTz(null, timezone) : dta).toISOString(true)
 80 | }
 81 | 
 82 | /**
 83 |  * @description return if entry string is a valid iso8601 data
 84 |  * @memberof business
 85 |  * @function
 86 |  * @param  {Moment} dta instantiate moment object
 87 |  * @returns {boolean} is valid?
 88 |  */
 89 | const isValidEntry = (dta) => {
 90 |   if (R.not(R.isNil(dta)) &&
 91 |     R.not(moment(dta, moment.ISO_8601).isValid())) {
 92 |     return false
 93 |   }
 94 |   return true
 95 | }
 96 | 
 97 | /**
 98 |  * Centralizando as configurações do moment
 99 |  */
100 | export { momentWithTz, toISOString, getDateFormated, isValidEntry }
101 | 
102 |
103 |
104 | 105 | 106 | 107 | 108 |
109 | 110 | 113 | 114 |
115 | 116 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /docs/business_todo.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: business/todo.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: business/todo.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
/**
 30 |  * reference only imports (for documentation)
 31 |  */
 32 | // eslint-disable-next-line no-unused-vars
 33 | import { Todo, MutateTodoInput, MutateTodoOutput } from './index'
 34 | /**
 35 |  * code imports
 36 |  */
 37 | import { v4 as uuidv4 } from 'uuid'
 38 | import { toISOString } from './moment'
 39 | import { ETodoStatus, EPriority } from './constants'
 40 | import R from 'ramda'
 41 | // import { EPriority, ETodoStatus } from './constants'
 42 | import {
 43 |   EClassError,
 44 |   throwCustomError,
 45 |   // eslint-disable-next-line no-unused-vars
 46 |   CustomError
 47 | } from '../utils'
 48 | 
 49 | /**
 50 |  * @description Validate a Todo event on creation
 51 |  * @memberof business
 52 |  * @function
 53 |  * @throws {CustomError}
 54 |  * @param {Todo} data imput data for create task
 55 |  * @param {string} owner owner of the task
 56 |  * @returns {Todo}
 57 |  */
 58 | export const validateCreateTodo = (data, owner) => {
 59 |   const creationDate = toISOString()
 60 |   const methodPath = 'business.todo.validateCreateTodo'
 61 | 
 62 |   if (R.isEmpty(data) || R.isNil(data)) {
 63 |     throwCustomError(new Error('invalid entry on field data, missing information'), methodPath, EClassError.USER_ERROR)
 64 |   }
 65 | 
 66 |   if (R.isEmpty(data.taskDescription) || R.isNil(data.taskDescription)) {
 67 |     throwCustomError(new Error('invalid entry on field data, missing information about taskDescription'), methodPath, EClassError.USER_ERROR)
 68 |   }
 69 | 
 70 |   if (R.isNil(owner)) {
 71 |     throwCustomError(new Error('owner is missing'), methodPath, EClassError.USER_ERROR)
 72 |   }
 73 | 
 74 |   if ((R.not(R.isNil(data.taskPriority)) && R.not(Object.values(EPriority).includes(data.taskPriority)))) {
 75 |     throwCustomError(new Error(`invalid value for priority: got ${data.taskPriority}`), methodPath, EClassError.USER_ERROR)
 76 |   }
 77 | 
 78 |   if ((R.not(R.isNil(data.taskStatus)) && R.not(Object.values(ETodoStatus).includes(data.taskStatus)))) {
 79 |     throwCustomError(new Error(`invalid value for status: got ${data.taskStatus}`), methodPath, EClassError.USER_ERROR)
 80 |   }
 81 | 
 82 |   return {
 83 |     // default values if is missing
 84 |     taskOrder: 0,
 85 |     taskPriority: EPriority.LOW,
 86 |     taskStatus: ETodoStatus.NEW,
 87 |     ...data,
 88 |     // information from system
 89 |     taskOwner: owner,
 90 |     creationDate,
 91 |     id: uuidv4()
 92 |   }
 93 | }
 94 | 
 95 | /**
 96 |    * @description Validate a Todo event on update
 97 |    * @memberof business
 98 |    * @function
 99 |    * @throws {CustomError}
100 |    * @param {MutateTodoInput} data update task input
101 |    * @param {Todo} originalData current task data
102 |    * @param {string} owner owner of the task
103 |    * @returns {MutateTodoOutput}
104 |    */
105 | export const validateUpdateTodo = (data, originalData, owner) => {
106 |   const lastUpdateDate = toISOString()
107 |   const methodPath = 'business.todo.validateUpdateTodo'
108 | 
109 |   if (R.isNil(originalData)) {
110 |     throwCustomError(new Error('no data for this id'), methodPath, EClassError.USER_ERROR)
111 |   }
112 | 
113 |   if (R.isEmpty(data) || R.isNil(data)) {
114 |     throwCustomError(new Error('invalid entry on field data, missing information'), methodPath, EClassError.USER_ERROR)
115 |   }
116 | 
117 |   if (R.isNil(owner)) {
118 |     throwCustomError(new Error('owner is missing'), methodPath, EClassError.USER_ERROR)
119 |   }
120 | 
121 |   return ['taskOwner', 'id', 'creationDate']
122 |     .reduce(
123 |       (reducedData, field) => R.dissoc(field, reducedData),
124 |       {
125 |         ...originalData,
126 |         ...data,
127 |         lastUpdateDate
128 |       }
129 |     )
130 | }
131 | 
132 | /**
133 |    * @description Validate a Todo event on delete
134 |    * @memberof business
135 |    * @function
136 |    * @throws {CustomError}
137 |    * @param {Todo} originalData current task data
138 |    * @param {string} owner owner of the task
139 |    * @returns {Todo}
140 |    */
141 | export const validateDeleteTodo = (originalData, owner) => {
142 |   const methodPath = 'business.todo.validateDeleteTodo'
143 |   if (R.isNil(originalData)) {
144 |     throwCustomError(new Error('no data for this id'), methodPath, EClassError.USER_ERROR)
145 |   }
146 | 
147 |   if (R.isNil(owner)) {
148 |     throwCustomError(new Error('owner is missing'), methodPath, EClassError.USER_ERROR)
149 |   }
150 | 
151 |   return originalData
152 | }
153 | 
154 |
155 |
156 | 157 | 158 | 159 | 160 |
161 | 162 | 165 | 166 |
167 | 168 | 171 | 172 | 173 | 174 | 175 | 176 | -------------------------------------------------------------------------------- /docs/config_environments.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: config/environments.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: config/environments.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
/**
30 |  * get environment variable
31 |  * @memberof config
32 |  * @param {string} env environment variable name
33 |  * @param {string} [defaultValue=''] default value
34 |  * @returns {string} environment variable value
35 |  */
36 | export const getEnv = (env, defaultValue = '') => process.env[env] || defaultValue
37 | 
38 |
39 |
40 | 41 | 42 | 43 | 44 |
45 | 46 | 49 | 50 |
51 | 52 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /docs/config_index.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: config/index.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: config/index.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
/**
 30 |  * config  Namespace.
 31 |  * @namespace config
 32 |  *
 33 |  *
 34 |  * @description this namespace is a configuration of the project
 35 |  */
 36 | // eslint-disable-next-line no-unused-vars
 37 | import { Configuration as Log4jsConf } from 'log4js'
 38 | 
 39 | // code imports
 40 | import { config } from 'dotenv'
 41 | import R from 'ramda'
 42 | import { getEnv } from './environments'
 43 | config()
 44 | 
 45 | /**
 46 |  * general aws configuration
 47 |  * @memberof config
 48 |  */
 49 | const AWSConfig = {
 50 |   accessKeyId: getEnv('AWS_ACCESS_KEY_ID'),
 51 |   secretAccessKey: getEnv('AWS_ACCESS_SECRET_KEY'),
 52 |   region: getEnv('AWS_REGION'),
 53 |   profile: getEnv('AWS_PROFILE')
 54 | }
 55 | 
 56 | /**
 57 |  * aws dynamodb configuration
 58 |  * @memberof config
 59 |  */
 60 | const AWSDynamoConfig = R.merge(
 61 |   AWSConfig,
 62 |   {
 63 |     region: getEnv('AWS_DYNAMO_REGION'),
 64 |     apiVersion: getEnv('AWS_DYNAMO_APIVERSION', '2012-08-10'),
 65 |     endpoint: getEnv('AWS_DYNAMO_ENDPOINT')
 66 |   }
 67 | )
 68 | 
 69 | /**
 70 |  * aws sqs configuration
 71 |  * @memberof config
 72 |  */
 73 | const AWSSqsConfig = R.merge(
 74 |   AWSConfig,
 75 |   {
 76 |     region: getEnv('AWS_SQS_REGION', 'us-west-2'),
 77 |     apiVersion: getEnv('AWS_SQS_APIVERSION', '2012-11-05')
 78 |   }
 79 | )
 80 | 
 81 | /**
 82 |  * aws s3 configuration
 83 |  * @memberof config
 84 |  */
 85 | const AWSS3Config = R.merge(
 86 |   AWSConfig,
 87 |   {
 88 |     region: getEnv('AWS_SQS_REGION', 'us-west-2'),
 89 |     apiVersion: getEnv('AWS_S3_APIVERSION', '2006-03-01')
 90 |   }
 91 | )
 92 | 
 93 | /**
 94 |  * moment configuration
 95 |  * @memberof config
 96 |  */
 97 | const momentConfig = {
 98 |   timezone: getEnv('TIMEZONE', 'America/Sao_Paulo')
 99 | }
100 | 
101 | const envProdName = 'production'
102 | 
103 | /**
104 |  * general application configuration
105 |  * @memberof config
106 |  */
107 | const appConfig = {
108 |   appName: getEnv('APP_NAME', 'hexagonal-boilerplate'),
109 |   isProduction: getEnv('NODE_ENV') === envProdName,
110 |   envName: getEnv('NODE_ENV'),
111 |   todo: {
112 |     tableName: getEnv('AWS_DYNAMO_TODO_TABLE_NAME', 'todos'),
113 |     queueUrl: getEnv('AWS_SQS_TODO_QUEUE_NAME', 'todo')
114 |   }
115 | }
116 | 
117 | /**
118 |  * logger configuration fixed for all jobs
119 |  * @memberof config
120 |  */
121 | const escribaConf = {
122 |   sensitiveConf: {
123 |     password: {
124 |       paths: ['message.password'],
125 |       pattern: /\w.*/g,
126 |       replacer: '*'
127 |     }
128 |   },
129 |   log4jsConf: {
130 |     appenders: {
131 |       out: {
132 |         type: 'console',
133 |         layout: {
134 |           type: 'pattern',
135 |           pattern: '[%d] %m'
136 |         }
137 |       }
138 |     },
139 |     categories: {
140 |       default: {
141 |         appenders: [
142 |           'out'
143 |         ],
144 |         level: 'info'
145 |       }
146 |     }
147 |   }
148 | }
149 | 
150 | export {
151 |   appConfig,
152 |   AWSConfig,
153 |   AWSDynamoConfig,
154 |   AWSS3Config,
155 |   AWSSqsConfig,
156 |   escribaConf,
157 |   envProdName,
158 |   momentConfig
159 | }
160 | 
161 |
162 |
163 | 164 | 165 | 166 | 167 |
168 | 169 | 172 | 173 |
174 | 175 | 178 | 179 | 180 | 181 | 182 | 183 | -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Bold-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/claytonsilva/nodejs-hexagonal-boilerplate/5f4df2cbf6a5e05a14c54fce88244f7f8d52b9b9/docs/fonts/OpenSans-Bold-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/claytonsilva/nodejs-hexagonal-boilerplate/5f4df2cbf6a5e05a14c54fce88244f7f8d52b9b9/docs/fonts/OpenSans-Bold-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-BoldItalic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/claytonsilva/nodejs-hexagonal-boilerplate/5f4df2cbf6a5e05a14c54fce88244f7f8d52b9b9/docs/fonts/OpenSans-BoldItalic-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-BoldItalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/claytonsilva/nodejs-hexagonal-boilerplate/5f4df2cbf6a5e05a14c54fce88244f7f8d52b9b9/docs/fonts/OpenSans-BoldItalic-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Italic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/claytonsilva/nodejs-hexagonal-boilerplate/5f4df2cbf6a5e05a14c54fce88244f7f8d52b9b9/docs/fonts/OpenSans-Italic-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Italic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/claytonsilva/nodejs-hexagonal-boilerplate/5f4df2cbf6a5e05a14c54fce88244f7f8d52b9b9/docs/fonts/OpenSans-Italic-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Light-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/claytonsilva/nodejs-hexagonal-boilerplate/5f4df2cbf6a5e05a14c54fce88244f7f8d52b9b9/docs/fonts/OpenSans-Light-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/claytonsilva/nodejs-hexagonal-boilerplate/5f4df2cbf6a5e05a14c54fce88244f7f8d52b9b9/docs/fonts/OpenSans-Light-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-LightItalic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/claytonsilva/nodejs-hexagonal-boilerplate/5f4df2cbf6a5e05a14c54fce88244f7f8d52b9b9/docs/fonts/OpenSans-LightItalic-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-LightItalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/claytonsilva/nodejs-hexagonal-boilerplate/5f4df2cbf6a5e05a14c54fce88244f7f8d52b9b9/docs/fonts/OpenSans-LightItalic-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/claytonsilva/nodejs-hexagonal-boilerplate/5f4df2cbf6a5e05a14c54fce88244f7f8d52b9b9/docs/fonts/OpenSans-Regular-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/claytonsilva/nodejs-hexagonal-boilerplate/5f4df2cbf6a5e05a14c54fce88244f7f8d52b9b9/docs/fonts/OpenSans-Regular-webfont.woff -------------------------------------------------------------------------------- /docs/global.html#CustomError: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Class: CustomError 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Class: CustomError

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 |
30 | 31 |

CustomError(err, methodPath, classError)

32 | 33 | 34 |
35 | 36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 |

new CustomError(err, methodPath, classError)

45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 |
Parameters:
60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 |
NameTypeDescription
err 88 | 89 | 90 | Error 91 | 92 | 93 | 94 | inherited error class
methodPath 111 | 112 | 113 | string 114 | 115 | 116 | 117 | method origin of the error
classError 134 | 135 | 136 | string 137 | 138 | 139 | 140 | class of error from our code
152 | 153 | 154 | 155 | 156 | 157 | 158 |
159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 |
Source:
186 |
189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 |
197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 |
219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 |
240 | 241 |
242 | 243 | 244 | 245 | 246 |
247 | 248 | 251 | 252 |
253 | 254 | 257 | 258 | 259 | 260 | 261 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Home 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Home

21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |

30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
51 | 52 | 55 | 56 |
57 | 58 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /docs/ports_aws-lambda_todo.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: ports/aws-lambda/todo.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: ports/aws-lambda/todo.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
import AWS from 'aws-sdk'
 30 | 
 31 | import { appConfig, AWSDynamoConfig } from '../../config'
 32 | import { adapter } from '../../adapters'
 33 | import { handleLogger } from '../logger/logger'
 34 | import { databaseRepository } from '../state-machines'
 35 | 
 36 | /**
 37 |  * Todo handler.
 38 |  * more about: https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html
 39 |  *
 40 |  * @memberof ports/aws/lambda
 41 |  * @param {*} event event object information from lambda (https://docs.aws.amazon.com/pt_br/lambda/latest/dg/with-s3.html)
 42 |  * @param {*} context information from direct call with params
 43 |  * @param {*} circuit breaker function
 44 |  */
 45 | export const handler = async (event, context) => {
 46 |   const appName = 'todo'
 47 |   const isProduction = process.env.ENV_NAME === 'production'
 48 |   const envName = isProduction ? 'production' : 'staging'
 49 | 
 50 |   // Escriba configuration.
 51 |   const escriba = handleLogger(appName, envName)
 52 | 
 53 |   // AWS Dynamo configuration.
 54 |   AWS.config.update(AWSDynamoConfig)
 55 |   const dynamo = new AWS.DynamoDB.DocumentClient()
 56 | 
 57 |   // inject repositories
 58 |   const databaseRepoInstance = databaseRepository(dynamo, appConfig.todo.tableName)
 59 |   const adapterInstance = adapter(escriba, databaseRepoInstance, null)
 60 | 
 61 |   const getTodo = async () => {
 62 |     try {
 63 |       const { id } = event.arguments
 64 |       const result = await adapterInstance.todo.getTodo(id)
 65 |       escriba.info('handler.get', `Get the task: ${id}`)
 66 |       return result
 67 |     } catch (error) {
 68 |       escriba.error('handler.generate', { ...error })
 69 |       throw error
 70 |     }
 71 |   }
 72 | 
 73 |   const createTodo = async () => {
 74 |     try {
 75 |       const { user } = event.arguments
 76 |       const result = await adapterInstance.todo.createTodo(event.arguments.data, user)
 77 |       escriba.info('handler.generate', `Generated the task: ${result.id}`, result)
 78 |       return result
 79 |     } catch (error) {
 80 |       escriba.error('handler.generate', { ...error })
 81 |       throw error
 82 |     }
 83 |   }
 84 | 
 85 |   const updateTodo = async () => {
 86 |     try {
 87 |       const { id, user } = event.arguments
 88 |       const result = await adapterInstance.todo.updateTodo(id, event.arguments.data, user)
 89 |       escriba.info('handler.generate', `Generated the task: ${result.id}`, result)
 90 |       return result
 91 |     } catch (error) {
 92 |       escriba.error('handler.generate', { ...error })
 93 |       throw error
 94 |     }
 95 |   }
 96 | 
 97 |   const deleteTodo = async () => {
 98 |     try {
 99 |       const { id } = event.arguments
100 |       const result = await adapterInstance.todo.deleteTodo(id)
101 |       escriba.info('handler.get', `Delete the task: ${id}`)
102 |       return result
103 |     } catch (error) {
104 |       escriba.error('handler.generate', { ...error })
105 |       throw error
106 |     }
107 |   }
108 | 
109 |   switch (event.field) {
110 |     case 'getTodo':
111 |       return getTodo()
112 |     case 'createTodo':
113 |       return createTodo()
114 |     case 'updateTodo':
115 |       return updateTodo()
116 |     case 'deleteTodo':
117 |       return deleteTodo()
118 |     default:
119 |       return getTodo()
120 |   }
121 | }
122 | 
123 |
124 |
125 | 126 | 127 | 128 | 129 |
130 | 131 | 134 | 135 |
136 | 137 | 140 | 141 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /docs/ports_http_routes_todo.router.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: ports/http/routes/todo.router.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: ports/http/routes/todo.router.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
/**
 30 |  * Reference only imports (for documentation).
 31 |  */
 32 | // eslint-disable-next-line no-unused-vars
 33 | import { Logger } from 'log4js'
 34 | // eslint-disable-next-line no-unused-vars
 35 | import { Adapter } from '../../../adapters/index'
 36 | 
 37 | /**
 38 |  * Code imports.
 39 |  */
 40 | import { Router } from 'express'
 41 | import { response } from './utils'
 42 | import { createTodo, deleteTodo, getTodo, updateTodo } from '../controllers/todo.controller'
 43 | 
 44 | const router = Router()
 45 | 
 46 | /**
 47 |  * @description Define the todo routes.
 48 |  *
 49 |  * @memberof ports/http/routes
 50 |  * @function
 51 |  * @param {Logger} escriba instance of escriba
 52 |  * @param {Adapter} adapter instantiated adapter
 53 |  * @returns {Router}
 54 |  */
 55 | 
 56 | export const todoRouter = (escriba, adapter) => {
 57 |   /**
 58 |    * get task with existing id
 59 |    */
 60 |   router.get('/:id', (req, res, next) => response(getTodo(escriba, adapter)(req, res, next), res, next))
 61 | 
 62 |   /**
 63 |    * create task with existing id
 64 |    */
 65 |   router.post('/', (req, res, next) => response(createTodo(escriba, adapter)(req, res, next), res, next))
 66 | 
 67 |   /**
 68 |    * update task with existing id
 69 |    */
 70 |   router.put('/:id', (req, res, next) => response(updateTodo(escriba, adapter)(req, res, next), res, next))
 71 | 
 72 |   /**
 73 |    * delete task with existing id
 74 |    */
 75 |   router.delete('/:id', (req, res, next) => response(deleteTodo(escriba, adapter)(req, res, next), res, next))
 76 | 
 77 |   return router
 78 | }
 79 | 
80 |
81 |
82 | 83 | 84 | 85 | 86 |
87 | 88 | 91 | 92 |
93 | 94 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /docs/scripts/linenumber.js: -------------------------------------------------------------------------------- 1 | /*global document */ 2 | (() => { 3 | const source = document.getElementsByClassName('prettyprint source linenums'); 4 | let i = 0; 5 | let lineNumber = 0; 6 | let lineId; 7 | let lines; 8 | let totalLines; 9 | let anchorHash; 10 | 11 | if (source && source[0]) { 12 | anchorHash = document.location.hash.substring(1); 13 | lines = source[0].getElementsByTagName('li'); 14 | totalLines = lines.length; 15 | 16 | for (; i < totalLines; i++) { 17 | lineNumber++; 18 | lineId = `line${lineNumber}`; 19 | lines[i].id = lineId; 20 | if (lineId === anchorHash) { 21 | lines[i].className += ' selected'; 22 | } 23 | } 24 | } 25 | })(); 26 | -------------------------------------------------------------------------------- /docs/scripts/prettify/Apache-License-2.0.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /docs/scripts/prettify/lang-css.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", 2 | /^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); 3 | -------------------------------------------------------------------------------- /docs/styles/jsdoc-default.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Open Sans'; 3 | font-weight: normal; 4 | font-style: normal; 5 | src: url('../fonts/OpenSans-Regular-webfont.eot'); 6 | src: 7 | local('Open Sans'), 8 | local('OpenSans'), 9 | url('../fonts/OpenSans-Regular-webfont.eot?#iefix') format('embedded-opentype'), 10 | url('../fonts/OpenSans-Regular-webfont.woff') format('woff'), 11 | url('../fonts/OpenSans-Regular-webfont.svg#open_sansregular') format('svg'); 12 | } 13 | 14 | @font-face { 15 | font-family: 'Open Sans Light'; 16 | font-weight: normal; 17 | font-style: normal; 18 | src: url('../fonts/OpenSans-Light-webfont.eot'); 19 | src: 20 | local('Open Sans Light'), 21 | local('OpenSans Light'), 22 | url('../fonts/OpenSans-Light-webfont.eot?#iefix') format('embedded-opentype'), 23 | url('../fonts/OpenSans-Light-webfont.woff') format('woff'), 24 | url('../fonts/OpenSans-Light-webfont.svg#open_sanslight') format('svg'); 25 | } 26 | 27 | html 28 | { 29 | overflow: auto; 30 | background-color: #fff; 31 | font-size: 14px; 32 | } 33 | 34 | body 35 | { 36 | font-family: 'Open Sans', sans-serif; 37 | line-height: 1.5; 38 | color: #4d4e53; 39 | background-color: white; 40 | } 41 | 42 | a, a:visited, a:active { 43 | color: #0095dd; 44 | text-decoration: none; 45 | } 46 | 47 | a:hover { 48 | text-decoration: underline; 49 | } 50 | 51 | header 52 | { 53 | display: block; 54 | padding: 0px 4px; 55 | } 56 | 57 | tt, code, kbd, samp { 58 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 59 | } 60 | 61 | .class-description { 62 | font-size: 130%; 63 | line-height: 140%; 64 | margin-bottom: 1em; 65 | margin-top: 1em; 66 | } 67 | 68 | .class-description:empty { 69 | margin: 0; 70 | } 71 | 72 | #main { 73 | float: left; 74 | width: 70%; 75 | } 76 | 77 | article dl { 78 | margin-bottom: 40px; 79 | } 80 | 81 | article img { 82 | max-width: 100%; 83 | } 84 | 85 | section 86 | { 87 | display: block; 88 | background-color: #fff; 89 | padding: 12px 24px; 90 | border-bottom: 1px solid #ccc; 91 | margin-right: 30px; 92 | } 93 | 94 | .variation { 95 | display: none; 96 | } 97 | 98 | .signature-attributes { 99 | font-size: 60%; 100 | color: #aaa; 101 | font-style: italic; 102 | font-weight: lighter; 103 | } 104 | 105 | nav 106 | { 107 | display: block; 108 | float: right; 109 | margin-top: 28px; 110 | width: 30%; 111 | box-sizing: border-box; 112 | border-left: 1px solid #ccc; 113 | padding-left: 16px; 114 | } 115 | 116 | nav ul { 117 | font-family: 'Lucida Grande', 'Lucida Sans Unicode', arial, sans-serif; 118 | font-size: 100%; 119 | line-height: 17px; 120 | padding: 0; 121 | margin: 0; 122 | list-style-type: none; 123 | } 124 | 125 | nav ul a, nav ul a:visited, nav ul a:active { 126 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 127 | line-height: 18px; 128 | color: #4D4E53; 129 | } 130 | 131 | nav h3 { 132 | margin-top: 12px; 133 | } 134 | 135 | nav li { 136 | margin-top: 6px; 137 | } 138 | 139 | footer { 140 | display: block; 141 | padding: 6px; 142 | margin-top: 12px; 143 | font-style: italic; 144 | font-size: 90%; 145 | } 146 | 147 | h1, h2, h3, h4 { 148 | font-weight: 200; 149 | margin: 0; 150 | } 151 | 152 | h1 153 | { 154 | font-family: 'Open Sans Light', sans-serif; 155 | font-size: 48px; 156 | letter-spacing: -2px; 157 | margin: 12px 24px 20px; 158 | } 159 | 160 | h2, h3.subsection-title 161 | { 162 | font-size: 30px; 163 | font-weight: 700; 164 | letter-spacing: -1px; 165 | margin-bottom: 12px; 166 | } 167 | 168 | h3 169 | { 170 | font-size: 24px; 171 | letter-spacing: -0.5px; 172 | margin-bottom: 12px; 173 | } 174 | 175 | h4 176 | { 177 | font-size: 18px; 178 | letter-spacing: -0.33px; 179 | margin-bottom: 12px; 180 | color: #4d4e53; 181 | } 182 | 183 | h5, .container-overview .subsection-title 184 | { 185 | font-size: 120%; 186 | font-weight: bold; 187 | letter-spacing: -0.01em; 188 | margin: 8px 0 3px 0; 189 | } 190 | 191 | h6 192 | { 193 | font-size: 100%; 194 | letter-spacing: -0.01em; 195 | margin: 6px 0 3px 0; 196 | font-style: italic; 197 | } 198 | 199 | table 200 | { 201 | border-spacing: 0; 202 | border: 0; 203 | border-collapse: collapse; 204 | } 205 | 206 | td, th 207 | { 208 | border: 1px solid #ddd; 209 | margin: 0px; 210 | text-align: left; 211 | vertical-align: top; 212 | padding: 4px 6px; 213 | display: table-cell; 214 | } 215 | 216 | thead tr 217 | { 218 | background-color: #ddd; 219 | font-weight: bold; 220 | } 221 | 222 | th { border-right: 1px solid #aaa; } 223 | tr > th:last-child { border-right: 1px solid #ddd; } 224 | 225 | .ancestors, .attribs { color: #999; } 226 | .ancestors a, .attribs a 227 | { 228 | color: #999 !important; 229 | text-decoration: none; 230 | } 231 | 232 | .clear 233 | { 234 | clear: both; 235 | } 236 | 237 | .important 238 | { 239 | font-weight: bold; 240 | color: #950B02; 241 | } 242 | 243 | .yes-def { 244 | text-indent: -1000px; 245 | } 246 | 247 | .type-signature { 248 | color: #aaa; 249 | } 250 | 251 | .name, .signature { 252 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 253 | } 254 | 255 | .details { margin-top: 14px; border-left: 2px solid #DDD; } 256 | .details dt { width: 120px; float: left; padding-left: 10px; padding-top: 6px; } 257 | .details dd { margin-left: 70px; } 258 | .details ul { margin: 0; } 259 | .details ul { list-style-type: none; } 260 | .details li { margin-left: 30px; padding-top: 6px; } 261 | .details pre.prettyprint { margin: 0 } 262 | .details .object-value { padding-top: 0; } 263 | 264 | .description { 265 | margin-bottom: 1em; 266 | margin-top: 1em; 267 | } 268 | 269 | .code-caption 270 | { 271 | font-style: italic; 272 | font-size: 107%; 273 | margin: 0; 274 | } 275 | 276 | .source 277 | { 278 | border: 1px solid #ddd; 279 | width: 80%; 280 | overflow: auto; 281 | } 282 | 283 | .prettyprint.source { 284 | width: inherit; 285 | } 286 | 287 | .source code 288 | { 289 | font-size: 100%; 290 | line-height: 18px; 291 | display: block; 292 | padding: 4px 12px; 293 | margin: 0; 294 | background-color: #fff; 295 | color: #4D4E53; 296 | } 297 | 298 | .prettyprint code span.line 299 | { 300 | display: inline-block; 301 | } 302 | 303 | .prettyprint.linenums 304 | { 305 | padding-left: 70px; 306 | -webkit-user-select: none; 307 | -moz-user-select: none; 308 | -ms-user-select: none; 309 | user-select: none; 310 | } 311 | 312 | .prettyprint.linenums ol 313 | { 314 | padding-left: 0; 315 | } 316 | 317 | .prettyprint.linenums li 318 | { 319 | border-left: 3px #ddd solid; 320 | } 321 | 322 | .prettyprint.linenums li.selected, 323 | .prettyprint.linenums li.selected * 324 | { 325 | background-color: lightyellow; 326 | } 327 | 328 | .prettyprint.linenums li * 329 | { 330 | -webkit-user-select: text; 331 | -moz-user-select: text; 332 | -ms-user-select: text; 333 | user-select: text; 334 | } 335 | 336 | .params .name, .props .name, .name code { 337 | color: #4D4E53; 338 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 339 | font-size: 100%; 340 | } 341 | 342 | .params td.description > p:first-child, 343 | .props td.description > p:first-child 344 | { 345 | margin-top: 0; 346 | padding-top: 0; 347 | } 348 | 349 | .params td.description > p:last-child, 350 | .props td.description > p:last-child 351 | { 352 | margin-bottom: 0; 353 | padding-bottom: 0; 354 | } 355 | 356 | .disabled { 357 | color: #454545; 358 | } 359 | -------------------------------------------------------------------------------- /docs/styles/prettify-jsdoc.css: -------------------------------------------------------------------------------- 1 | /* JSDoc prettify.js theme */ 2 | 3 | /* plain text */ 4 | .pln { 5 | color: #000000; 6 | font-weight: normal; 7 | font-style: normal; 8 | } 9 | 10 | /* string content */ 11 | .str { 12 | color: #006400; 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | 17 | /* a keyword */ 18 | .kwd { 19 | color: #000000; 20 | font-weight: bold; 21 | font-style: normal; 22 | } 23 | 24 | /* a comment */ 25 | .com { 26 | font-weight: normal; 27 | font-style: italic; 28 | } 29 | 30 | /* a type name */ 31 | .typ { 32 | color: #000000; 33 | font-weight: normal; 34 | font-style: normal; 35 | } 36 | 37 | /* a literal value */ 38 | .lit { 39 | color: #006400; 40 | font-weight: normal; 41 | font-style: normal; 42 | } 43 | 44 | /* punctuation */ 45 | .pun { 46 | color: #000000; 47 | font-weight: bold; 48 | font-style: normal; 49 | } 50 | 51 | /* lisp open bracket */ 52 | .opn { 53 | color: #000000; 54 | font-weight: bold; 55 | font-style: normal; 56 | } 57 | 58 | /* lisp close bracket */ 59 | .clo { 60 | color: #000000; 61 | font-weight: bold; 62 | font-style: normal; 63 | } 64 | 65 | /* a markup tag name */ 66 | .tag { 67 | color: #006400; 68 | font-weight: normal; 69 | font-style: normal; 70 | } 71 | 72 | /* a markup attribute name */ 73 | .atn { 74 | color: #006400; 75 | font-weight: normal; 76 | font-style: normal; 77 | } 78 | 79 | /* a markup attribute value */ 80 | .atv { 81 | color: #006400; 82 | font-weight: normal; 83 | font-style: normal; 84 | } 85 | 86 | /* a declaration */ 87 | .dec { 88 | color: #000000; 89 | font-weight: bold; 90 | font-style: normal; 91 | } 92 | 93 | /* a variable name */ 94 | .var { 95 | color: #000000; 96 | font-weight: normal; 97 | font-style: normal; 98 | } 99 | 100 | /* a function name */ 101 | .fun { 102 | color: #000000; 103 | font-weight: bold; 104 | font-style: normal; 105 | } 106 | 107 | /* Specify class=linenums on a pre to get line numbering */ 108 | ol.linenums { 109 | margin-top: 0; 110 | margin-bottom: 0; 111 | } 112 | -------------------------------------------------------------------------------- /docs/styles/prettify-tomorrow.css: -------------------------------------------------------------------------------- 1 | /* Tomorrow Theme */ 2 | /* Original theme - https://github.com/chriskempson/tomorrow-theme */ 3 | /* Pretty printing styles. Used with prettify.js. */ 4 | /* SPAN elements with the classes below are added by prettyprint. */ 5 | /* plain text */ 6 | .pln { 7 | color: #4d4d4c; } 8 | 9 | @media screen { 10 | /* string content */ 11 | .str { 12 | color: #718c00; } 13 | 14 | /* a keyword */ 15 | .kwd { 16 | color: #8959a8; } 17 | 18 | /* a comment */ 19 | .com { 20 | color: #8e908c; } 21 | 22 | /* a type name */ 23 | .typ { 24 | color: #4271ae; } 25 | 26 | /* a literal value */ 27 | .lit { 28 | color: #f5871f; } 29 | 30 | /* punctuation */ 31 | .pun { 32 | color: #4d4d4c; } 33 | 34 | /* lisp open bracket */ 35 | .opn { 36 | color: #4d4d4c; } 37 | 38 | /* lisp close bracket */ 39 | .clo { 40 | color: #4d4d4c; } 41 | 42 | /* a markup tag name */ 43 | .tag { 44 | color: #c82829; } 45 | 46 | /* a markup attribute name */ 47 | .atn { 48 | color: #f5871f; } 49 | 50 | /* a markup attribute value */ 51 | .atv { 52 | color: #3e999f; } 53 | 54 | /* a declaration */ 55 | .dec { 56 | color: #f5871f; } 57 | 58 | /* a variable name */ 59 | .var { 60 | color: #c82829; } 61 | 62 | /* a function name */ 63 | .fun { 64 | color: #4271ae; } } 65 | /* Use higher contrast and text-weight for printable form. */ 66 | @media print, projection { 67 | .str { 68 | color: #060; } 69 | 70 | .kwd { 71 | color: #006; 72 | font-weight: bold; } 73 | 74 | .com { 75 | color: #600; 76 | font-style: italic; } 77 | 78 | .typ { 79 | color: #404; 80 | font-weight: bold; } 81 | 82 | .lit { 83 | color: #044; } 84 | 85 | .pun, .opn, .clo { 86 | color: #440; } 87 | 88 | .tag { 89 | color: #006; 90 | font-weight: bold; } 91 | 92 | .atn { 93 | color: #404; } 94 | 95 | .atv { 96 | color: #060; } } 97 | /* Style */ 98 | /* 99 | pre.prettyprint { 100 | background: white; 101 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 102 | font-size: 12px; 103 | line-height: 1.5; 104 | border: 1px solid #ccc; 105 | padding: 10px; } 106 | */ 107 | 108 | /* Specify class=linenums on a pre to get line numbering */ 109 | ol.linenums { 110 | margin-top: 0; 111 | margin-bottom: 0; } 112 | 113 | /* IE indents via margin-left */ 114 | li.L0, 115 | li.L1, 116 | li.L2, 117 | li.L3, 118 | li.L4, 119 | li.L5, 120 | li.L6, 121 | li.L7, 122 | li.L8, 123 | li.L9 { 124 | /* */ } 125 | 126 | /* Alternate shading for lines */ 127 | li.L1, 128 | li.L3, 129 | li.L5, 130 | li.L7, 131 | li.L9 { 132 | /* */ } 133 | -------------------------------------------------------------------------------- /docs/utils.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Namespace: utils 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Namespace: utils

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 |
30 | 31 |

utils

32 | 33 | 34 |
35 | 36 |
37 |
38 | 39 | 40 |
this namespace have common methods to use in all scope from repo
41 | 42 | 43 | 44 | 45 | 46 |
47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 |
Source:
74 |
77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 |
85 | 86 | 87 | 88 | 89 |
90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 |

Members

105 | 106 | 107 | 108 |

(static, constant) exports.EClassError :string

109 | 110 | 111 | 112 | 113 |
114 | Enum for EClassError values. 115 |
116 | 117 | 118 | 119 |
Type:
120 |
    121 |
  • 122 | 123 | string 124 | 125 | 126 |
  • 127 |
128 | 129 | 130 | 131 | 132 | 133 |
134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 |
Source:
161 |
164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 |
172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 |

Methods

183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 |

(static) exports.throwCustomError(err, methodPath, classError) → {undefined}

191 | 192 | 193 | 194 | 195 | 196 | 197 |
198 | Throw correct messagem error from origin in all levels 199 | of the structure business -> adapter -> ports with correct CustomError 200 | if the error is typeof CustomError 201 |
202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 |
Parameters:
212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 |
NameTypeDescription
err 240 | 241 | 242 | Error 243 | 244 | 245 | 246 | inherited error class
methodPath 263 | 264 | 265 | string 266 | 267 | 268 | 269 | method origin of the error
classError 286 | 287 | 288 | string 289 | 290 | 291 | 292 | class of error from our code
304 | 305 | 306 | 307 | 308 | 309 | 310 |
311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 |
Source:
338 |
341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 |
349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 |
Returns:
365 | 366 | 367 | 368 | 369 |
370 |
371 | Type 372 |
373 |
374 | 375 | undefined 376 | 377 | 378 |
379 |
380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 |
394 | 395 |
396 | 397 | 398 | 399 | 400 |
401 | 402 | 405 | 406 |
407 | 408 | 411 | 412 | 413 | 414 | 415 | -------------------------------------------------------------------------------- /docs/utils_errors.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: utils/errors.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: utils/errors.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
/**
 30 |  * common treatments for errors handler
 31 |  *
 32 |  * this only one mutable object in all code
 33 |  */
 34 | 
 35 | /**
 36 |  * Object type for Custom errors
 37 |  * @typedef {Object} CustomError Object
 38 |  * @property {string} name - class of error from outside.
 39 |  * @property {string} internalName - class of error from inside (like BadRequest, etc.).
 40 |  * @property {string} methodPath - method origin of the error
 41 |  * @property {string} message - detail of error text
 42 |  * @property {string} stack - Stack call trace from base code call
 43 |  */
 44 | export class CustomError extends Error {
 45 |   /**
 46 |    * @constructs CustomError
 47 |    * @param {Error} err inherited error class
 48 |    * @param {string} methodPath  method origin of the error
 49 |    * @param {string} classError class of error from our code
 50 |    */
 51 |   constructor(err, methodPath, classError) {
 52 |     super()
 53 |     const { name, message, stack } = err
 54 | 
 55 |     // eslint-disable-next-line
 56 |     this.name = name
 57 |     // eslint-disable-next-line
 58 |     this.message = message
 59 |     // eslint-disable-next-line
 60 |     this.stack = stack
 61 |     // eslint-disable-next-line
 62 |     this.internalName = classError
 63 |     // eslint-disable-next-line
 64 |     this.method = methodPath
 65 |   }
 66 | }
 67 | 
 68 | /**
 69 |  * Enum for EClassError values.
 70 |  * @readonly
 71 |  * @memberof utils
 72 |  * @enum {string}
 73 |  */
 74 | export const EClassError = {
 75 |   INTERNAL: 'INTERNAL',
 76 |   USER_ERROR: 'USER_ERROR'
 77 | }
 78 | 
 79 | /**
 80 |  * Throw correct messagem error from origin in all levels
 81 |  * of the structure business -> adapter -> ports with correct CustomError
 82 |  * if the error is typeof CustomError
 83 |  * @memberof utils
 84 |  * @function
 85 |  * @param {Error} err inherited error class
 86 |  * @param {string} methodPath  method origin of the error
 87 |  * @param {string} classError class of error from our code
 88 |  * @return {undefined}
 89 |  */
 90 | export const throwCustomError = (error, methodPath, classError) => {
 91 |   switch (error.constructor) {
 92 |     case CustomError:
 93 |       throw error
 94 |     default:
 95 |       throw new CustomError(error, methodPath, classError)
 96 |   }
 97 | }
 98 | 
99 |
100 |
101 | 102 | 103 | 104 | 105 |
106 | 107 | 110 | 111 |
112 | 113 | 116 | 117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /docs/utils_index.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: utils/index.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: utils/index.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
/**
30 |  * Utils  Namespace.
31 |  * @namespace utils
32 |  *
33 |  *
34 |  * @description this namespace have common methods to use in all scope from repo
35 |  */
36 | 
37 | import { CustomError, EClassError, throwCustomError } from './errors'
38 | 
39 | export { CustomError, EClassError, throwCustomError }
40 | 
41 |
42 |
43 | 44 | 45 | 46 | 47 |
48 | 49 | 52 | 53 |
54 | 55 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /hexagonal-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/claytonsilva/nodejs-hexagonal-boilerplate/5f4df2cbf6a5e05a14c54fce88244f7f8d52b9b9/hexagonal-architecture.png -------------------------------------------------------------------------------- /iaac/localstack/lambdas.tf: -------------------------------------------------------------------------------- 1 | resource "aws_lambda_function" "todo" { 2 | function_name = "todo_tf_handler" 3 | handler = "/var/task/dist/ports/aws-lambda/todo.handler" 4 | runtime = "${var.runtime}" 5 | memory_size = "128" 6 | timeout = 10 #for local environemnt this is larger than 10 seconds 7 | 8 | role = "arn:aws:iam::123456:role/irrelevant" 9 | s3_bucket = "__local__" 10 | s3_key = "${path.module}/../../" 11 | 12 | environment = { 13 | variables = "${local.environent}" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /iaac/localstack/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | 3 | dynamodb_table_name_todo = "todos" 4 | 5 | localstack_remote_endpoint = "http://localstack_${var.project_name}:4566" 6 | 7 | environent = { 8 | ################################### 9 | # GLOBAL AND DEBUG ENVS 10 | ################################### 11 | APP_NAME = "${var.project_name}" 12 | ENV_NAME = "development" 13 | TIMEZONE = "America/Sao_Paulo" 14 | 15 | ################################### 16 | # AWS SERVICES 17 | ################################### 18 | # DYNAMO 19 | AWS_DYNAMO_REGION = "${var.region}" 20 | 21 | AWS_DYNAMO_APIVERSION = "${var.versions["DYNAMODB"]}" 22 | AWS_DYNAMO_TODO_TABLE_NAME = "${local.dynamodb_table_name_todo}" 23 | 24 | # SQS 25 | AWS_SQS_REGION = "${var.region}" 26 | 27 | AWS_SQS_APIVERSION = "${var.versions["SQS"]}" 28 | 29 | 30 | # SERVICES ENDPOINTS INNER LAMBDA DOCKER 31 | AWS_DYNAMO_ENDPOINT = "${local.localstack_remote_endpoint}" 32 | AWS_S3_URL = "${local.localstack_remote_endpoint}" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /iaac/localstack/outputs.tf: -------------------------------------------------------------------------------- 1 | # this file is for output members 2 | 3 | output "todo_queue_url" { 4 | value = "${aws_sqs_queue.todo_queue.id}" 5 | } 6 | 7 | 8 | output "dynamo_todo_id" { 9 | value = "${aws_dynamodb_table.todo_data.id}" 10 | } 11 | -------------------------------------------------------------------------------- /iaac/localstack/provider.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = "${var.region}" 3 | 4 | endpoints { 5 | dynamodb = "http://localhost:4566" 6 | s3 = "http://localhost:4566" 7 | sqs = "http://localhost:4566" 8 | lambda = "http://localhost:4566" 9 | apigateway = "http://localhost:4566" 10 | 11 | # sts for full experience without valid real profile on aws 12 | sts = "http://localhost:4566" 13 | } 14 | 15 | version = "~> 2.62" 16 | 17 | profile = "localstack" 18 | } 19 | -------------------------------------------------------------------------------- /iaac/localstack/state-machines.tf: -------------------------------------------------------------------------------- 1 | resource "aws_dynamodb_table" "todo_data" { 2 | name = "${local.dynamodb_table_name_todo}" 3 | read_capacity = 5 4 | write_capacity = 1 5 | hash_key = "id" 6 | 7 | attribute { 8 | name = "id" 9 | type = "S" 10 | } 11 | } 12 | 13 | resource "aws_sqs_queue" "todo_queue" { 14 | name = "todo-queue" 15 | delay_seconds = 90 16 | max_message_size = 8192 17 | message_retention_seconds = 3600 18 | receive_wait_time_seconds = 10 19 | } 20 | -------------------------------------------------------------------------------- /iaac/localstack/variables.tf: -------------------------------------------------------------------------------- 1 | variable "region" { 2 | default = "us-east-1" 3 | } 4 | 5 | variable "project_name" { 6 | type = "string" 7 | 8 | default = "hexagonal_boilerplate" 9 | } 10 | 11 | variable "runtime" { 12 | type = "string" 13 | default = "nodejs12.x" 14 | } 15 | 16 | variable "versions" { 17 | type = "map" 18 | 19 | default = { 20 | SQS = "2012-11-05" 21 | S3 = "2006-03-01" 22 | DYNAMODB = "2012-08-10" 23 | } 24 | } 25 | 26 | variable "environment" { 27 | type = "string" 28 | 29 | default = "development" 30 | } 31 | 32 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | // Automatically clear mock calls and instances between every test 4 | clearMocks: true, 5 | 6 | // Indicates whether the coverage information should be collected while executing the test 7 | collectCoverage: true, 8 | 9 | // The directory where Jest should output its coverage files 10 | coverageDirectory: "//reports//coverage", 11 | 12 | // A list of reporter names that Jest uses when writing coverage reports 13 | coverageReporters: [ 14 | "text", 15 | "lcov", 16 | "html" 17 | ], 18 | 19 | // The root directory that Jest should scan for tests and modules within 20 | rootDir: ".", 21 | roots: [ 22 | "/src" 23 | ], 24 | 25 | // The test environment that will be used for testing 26 | testEnvironment: "node", 27 | 28 | // The glob patterns Jest uses to detect test files 29 | testMatch: [ 30 | "**/*.spec.js" 31 | ], 32 | 33 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 34 | testPathIgnorePatterns: [ 35 | "/node_modules/" 36 | ], 37 | 38 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 39 | // watchPathIgnorePatterns: [], 40 | 41 | // Whether to use watchman for file crawling 42 | // watchman: true, 43 | } 44 | -------------------------------------------------------------------------------- /jsdoc.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "templates": { 3 | "default": { 4 | "includeDate": false 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /mocks/lambda/events/create_todo.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": "{\"message\": \"hello world\"}", 3 | "field": "createTodo", 4 | "resource": "/{proxy+}", 5 | "arguments": { 6 | "user": "testUser", 7 | "data": { 8 | "taskDescription": "first todo", 9 | "taskStatus": "NEW", 10 | "taskPriority": "LOW", 11 | "taskOrder": 0 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-hexagonal-boilerplate", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "prebuild": "rimraf dist", 7 | "build": "babel src -d dist --source-maps", 8 | "watch": "babel src -w -d dist --source-maps", 9 | "depcheck": "depcheck", 10 | "test-ci": "ENV_NAME=test jest --ci --reporters=jest-junit --outputFile=./test-results/jest", 11 | "start": "node dist/ports/http/bin/www", 12 | "test": "ENV_NAME=test jest", 13 | "lint": "eslint -c .eslintrc src/**/*.js", 14 | "local-lambda": "serverless offline -s dev", 15 | "prebuild-lambda": "yarn build", 16 | "build-lambda": "serverless package", 17 | "stryker-mutate": "stryker run", 18 | "doc": "jsdoc ./src/**/*.js -d ./docs -c ./jsdoc.conf.json" 19 | }, 20 | "jest": { 21 | "verbose": true 22 | }, 23 | "jest-junit": { 24 | "suiteName": "jest tests", 25 | "outputDirectory": "./test-results/jest", 26 | "outputName": "junit.xml", 27 | "uniqueOutputName": "false", 28 | "classNameTemplate": "{classname}-{title}", 29 | "titleTemplate": "{classname}-{title}", 30 | "ancestorSeparator": " › ", 31 | "usePathForSuiteName": "true" 32 | }, 33 | "engines": { 34 | "node": "^12.18.1", 35 | "yarn": "^1.20.0", 36 | "npm": "^6.0.0" 37 | }, 38 | "lint-staged": { 39 | "src/**/*.js": [ 40 | "eslint --fix", 41 | "yarn doc", 42 | "git add" 43 | ] 44 | }, 45 | "husky": { 46 | "hooks": { 47 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 48 | "pre-commit": "lint-staged" 49 | } 50 | }, 51 | "dependencies": { 52 | "aws-sdk": "^2.582.0", 53 | "body-parser": "^1.19.0", 54 | "cuid": "^2.1.8", 55 | "debug": "^4.1.1", 56 | "escriba": "^2.5.0", 57 | "express": "^4.17.1", 58 | "log4js": "^5.1.0", 59 | "moment": "^2.24.0", 60 | "moment-timezone": "^0.5.21", 61 | "ramda": "^0.26.1", 62 | "uuid": "^7.0.2" 63 | }, 64 | "devDependencies": { 65 | "@babel/cli": "^7.2.3", 66 | "@babel/core": "^7.8.7", 67 | "@babel/preset-env": "^7.2.3", 68 | "@commitlint/cli": "^7.2.1", 69 | "@commitlint/config-conventional": "^7.1.2", 70 | "@stryker-mutator/core": "^3.2.4", 71 | "@stryker-mutator/javascript-mutator": "^3.2.4", 72 | "@stryker-mutator/jest-runner": "^3.2.4", 73 | "@types/body-parser": "^1.17.1", 74 | "@types/express": "^4.17.2", 75 | "@types/jest": "^25.1.4", 76 | "@types/ramda": "0.26.1", 77 | "babel-core": "^7.0.0-bridge.0", 78 | "babel-eslint": "^10.0.1", 79 | "babel-jest": "^25.1.0", 80 | "commitlint": "^7.2.1", 81 | "depcheck": "^0.9.2", 82 | "dotenv": "^8.2.0", 83 | "eslint": "^6.8.0", 84 | "eslint-config-standard": "^12.0.0", 85 | "eslint-plugin-fp": "2.3.0", 86 | "eslint-plugin-immutable": "^1.0.0", 87 | "eslint-plugin-import": "^2.18.2", 88 | "eslint-plugin-jest": "^23.8.2", 89 | "eslint-plugin-jsdoc": "^21.0.0", 90 | "eslint-plugin-node": "^8.0.0", 91 | "eslint-plugin-promise": "^4.2.1", 92 | "eslint-plugin-standard": "^4.0.1", 93 | "husky": "^1.3.1", 94 | "jest": "^25.1.0", 95 | "jest-junit": "^10.0.0", 96 | "jsdoc": "^3.6.3", 97 | "lint-staged": "^8.1.0", 98 | "prettier-eslint": "8.8.2", 99 | "rimraf": "^3.0.0", 100 | "serverless": "^1.70.0", 101 | "serverless-dotenv-plugin": "^2.4.2", 102 | "serverless-jetpack": "^0.10.6", 103 | "serverless-localstack": "^0.4.24", 104 | "serverless-offline": "^6.1.5", 105 | "standard": "^14.3.1" 106 | }, 107 | "resolutions": { 108 | "handlebars": "^4.3.0", 109 | "eslint-utils": "^1.4.1", 110 | "lodash": "^4.17.15", 111 | "minimist": "^1.2.2", 112 | "dd-trace": "^0.14.0", 113 | "mkdirp": "^1.0.3", 114 | "typescript": ">=2.8.0", 115 | "debug": "^4.1.1", 116 | "yargs-parser": "^18.1.2", 117 | "bl": ">=4.0.3", 118 | "node-fetch": ">=2.6.1", 119 | "dot-prop": ">=4.2.1" 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /serverless.yaml: -------------------------------------------------------------------------------- 1 | # Serverless packaging! 2 | # 3 | service: todo 4 | 5 | plugins: 6 | - serverless-offline 7 | - serverless-localstack 8 | - serverless-dotenv-plugin 9 | - serverless-jetpack 10 | custom: 11 | dotenv: 12 | # dot env to serve environment variables in files 13 | path: .env.dev 14 | logging: false 15 | localstack: 16 | # localstack environment 17 | # this environment can setup localstack environment on lambda functions using serverless-localstack 18 | # you can use instead terraform formation for environment tests 19 | stages: 20 | # list of stages for which the plugin should be enabled 21 | - local 22 | - development 23 | host: http://localhost # optional - LocalStack host to connect to 24 | autostart: false # optional - start LocalStack in Docker on Serverless deploy 25 | endpoints: 26 | # This section is optional - can be used for customizing the target endpoints 27 | S3: http://localhost:4566 28 | DynamoDB: http://localhost:4566 29 | CloudFormation: http://localhost:4566 30 | Elasticsearch: http://localhost:4566 31 | ES: http://localhost:4566 32 | SNS: http://localhost:4566 33 | SQS: http://localhost:4566 34 | Lambda: http://localhost:4566 35 | Kinesis: http://localhost:4566 36 | STS: http://localhost:4566 37 | IAM: http://localhost:4566 38 | lambda: 39 | # Enable this flag to improve performance 40 | mountCode: True 41 | docker: 42 | # Enable this flag to run "docker ..." commands as sudo 43 | sudo: False 44 | stages: 45 | - local 46 | - dev 47 | provider: 48 | name: aws 49 | runtime: nodejs12.x 50 | functions: 51 | todo: # A Function 52 | handler: dist/ports/aws-lambda/todo.handler # The file and module for this specific function. 53 | name: todo # optional, Deployed Lambda name 54 | description: todo # The description of your function. 55 | runtime: nodejs12.x # Runtime for this specific function. Overrides the default which is set on the provider level 56 | timeout: 20 # optional, in seconds, default is 6, we recomend 10 seconds for local environment 57 | package: 58 | include: # Specify the directories and files which should be included in the deployment package for this specific function. 59 | - dist/** 60 | exclude: # Specify the directories and files which should be excluded in the deployment package for this specific function. 61 | - src/** 62 | - .*/** 63 | - .* 64 | - iaac/** 65 | - mocks/** 66 | - docs/** 67 | - docker-compose.yml 68 | - Dockerfile 69 | - jest.config.js 70 | - jsdoc.conf.json 71 | individually: true # Enables individual packaging for specific function. If true you must provide package for each function. Defaults to false 72 | excludeDevDependencies: true 73 | -------------------------------------------------------------------------------- /src/adapters/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Adapters Namespace. 3 | * @namespace adapters 4 | * 5 | * 6 | * @description this namespace control communication between business and state-machines 7 | */ 8 | 9 | /** 10 | * @typedef {Object} Adapter 11 | * @property {TodoAdapter} todo todo adapter instantied 12 | */ 13 | 14 | // eslint-disable-next-line no-unused-vars 15 | import { DynamoRepositoryInstance } from '../ports/state-machines' 16 | // code imports 17 | import todoAdapterFactory, 18 | // eslint-disable-next-line no-unused-vars 19 | { TodoAdapter } from './todo' 20 | 21 | /** 22 | * @description dynamo repository for state machine 23 | * 24 | * @memberof ports/state-machines 25 | * @function 26 | * @param {Logger} escriba - Instance of escriba. 27 | * @param {DynamoRepositoryInstance} repository repository instatiated 28 | * @returns {Adapter} 29 | */ 30 | export const adapter = (escriba, repository) => { 31 | return { 32 | todo: todoAdapterFactory(escriba, repository) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/adapters/todo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Reference only imports (for documentation) 3 | */ 4 | 5 | // eslint-disable-next-line no-unused-vars 6 | import { Logger } from 'log4js' 7 | // eslint-disable-next-line no-unused-vars 8 | import { DynamoRepositoryInstance } from '../ports/state-machines' 9 | // eslint-disable-next-line no-unused-vars 10 | import { MutateTodoInput, Todo, TodoKey } from '../business' 11 | 12 | /** 13 | * code imports 14 | */ 15 | 16 | import { 17 | // eslint-disable-next-line no-unused-vars 18 | CustomError, 19 | EClassError, 20 | throwCustomError 21 | } from '../utils' 22 | 23 | import { validateUpdateTodo, validateCreateTodo, validateDeleteTodo } from '../business/todo' 24 | 25 | /** 26 | * @description Todo adapter factory 27 | * @memberof adapters 28 | * @function 29 | * @param {Logger} escriba instance of escriba logger 30 | * @param {DynamoRepositoryInstance} repository state-machine database methods 31 | * @returns {TodoAdapter} todo adapter instantied 32 | */ 33 | const todoAdapterFactory = (escriba, repository) => ({ 34 | getTodo: getTodo(repository), 35 | createTodo: createTodo(escriba, repository), 36 | updateTodo: updateTodo(escriba, repository), 37 | deleteTodo: deleteTodo(escriba, repository) 38 | }) 39 | 40 | export default todoAdapterFactory 41 | 42 | /** 43 | * @description Handler function to get todo data by id . 44 | * @memberof adapters 45 | * @async 46 | * @function 47 | * @throws {CustomError} 48 | * @param {DynamoRepositoryInstance} repository - State-machine database methods. 49 | * @returns {getTodoReturn} GetDocument method ready to execute. 50 | */ 51 | const getTodo = (repository) => async (id) => { 52 | const methodPath = 'adapters.todo.getTodo' 53 | try { 54 | return await repository.getDocument({ id }) 55 | } catch (error) { 56 | throwCustomError(error, methodPath, EClassError.INTERNAL) 57 | } 58 | } 59 | 60 | /** 61 | * @description Create todo in the DynamoDB. 62 | * @memberof adapters 63 | * @async 64 | * @function 65 | * @throws {CustomError} 66 | * @param {Logger} escriba instance of escriba 67 | * @param {DynamoRepositoryInstance} repository state-machine database methods 68 | * @returns {createTodoReturn} function to call createTodo direct 69 | */ 70 | const createTodo = (escriba, repository) => async (params, user) => { 71 | const methodPath = 'adapters.todo.createTodo' 72 | try { 73 | const documentInserted = await repository 74 | .putDocument( 75 | validateCreateTodo( 76 | params, 77 | user 78 | ) 79 | ) 80 | 81 | escriba.info({ 82 | action: 'TASK_CREATED', 83 | method: methodPath, 84 | data: { documentInserted } 85 | }) 86 | 87 | return documentInserted 88 | } catch (error) { 89 | throwCustomError(error, methodPath, EClassError.INTERNAL) 90 | } 91 | } 92 | 93 | /** 94 | * @description Update todo in the DynamoDB. 95 | * @memberof adapters 96 | * @async 97 | * @function 98 | * @throws {CustomError} 99 | * @param {Logger} escriba instance of escriba 100 | * @param {DynamoRepositoryInstance} repository state-machine database methods 101 | * @returns {updateTodoReturn} function to call updateTodo direct 102 | */ 103 | const updateTodo = (escriba, repository) => async (id, params, user) => { 104 | const methodPath = 'adapters.todo.updateTodo' 105 | try { 106 | const currObject = await getTodo(repository)(id) 107 | 108 | const ExpressionAttributeValues = validateUpdateTodo(params, currObject, user) 109 | 110 | const UpdateExpression = ` 111 | set taskOrder = :taskOrder, 112 | taskDescription = :taskDescription, 113 | taskStatus = :taskStatus, 114 | taskPriority = :taskPriority, 115 | lastUpdateDate = :lastUpdateDate 116 | ` 117 | // send report to existing todo previous created 118 | const task = await repository.updateDocument( 119 | { id }, 120 | UpdateExpression, 121 | ExpressionAttributeValues 122 | ) 123 | 124 | // log report data 125 | escriba.info({ 126 | action: 'TASK_UPDATED', 127 | method: methodPath, 128 | data: task 129 | }) 130 | 131 | // return updated item 132 | return task 133 | } catch (error) { 134 | throwCustomError(error, methodPath, EClassError.INTERNAL) 135 | } 136 | } 137 | 138 | /** 139 | * @description delete todo in the DynamoDB. 140 | * @memberof adapters 141 | * @async 142 | * @function 143 | * @throws {CustomError} 144 | * @param {Logger} escriba instance of escriba 145 | * @param {DynamoRepositoryInstance} repository state-machine database methods 146 | * @returns {deleteTodoReturn} function to call deleteTodo direct 147 | */ 148 | const deleteTodo = (escriba, repository) => async (id, user) => { 149 | const methodPath = 'adapters.todo.deleteTodo' 150 | try { 151 | const currObject = validateDeleteTodo(await getTodo(repository)(id), user) 152 | await repository.deleteDocument({ id }) 153 | 154 | // log report data 155 | escriba.info({ 156 | action: 'TASK_DELETED', 157 | method: methodPath, 158 | data: currObject 159 | }) 160 | 161 | return currObject 162 | } catch (error) { 163 | throwCustomError(error, methodPath, EClassError.INTERNAL) 164 | } 165 | } 166 | 167 | /** 168 | * complex callbacks documentation 169 | * 170 | */ 171 | 172 | /** 173 | * @typedef {Object} TodoAdapter 174 | * @property {getTodoReturn} getTodo function to get task by id (instantied) 175 | * @property {createTodoReturn} createTodo function to generate task (instantiated). 176 | * @property {updateTodoReturn} updateTodo function to update task (instantiated). 177 | * @property {deleteTodoReturn} deleteTodo function to delete task (instantiated). 178 | */ 179 | 180 | /** 181 | * This callback is displayed as part of the createTodo function. 182 | * @memberof adapters 183 | * @callback createTodoReturn 184 | * @param {MutateTodoInput} params input param for createTodo 185 | * @param {string} owner of the data entry logged 186 | * @returns {Promise} new report data 187 | */ 188 | 189 | /** 190 | * This callback is displayed as part of the updateTodo function. 191 | * @memberof adapters 192 | * @callback updateTodoReturn 193 | * @param {string} id id of the current data for update 194 | * @param {MutateTodoInput} params input param for updateTodo 195 | * @param {string} owner of the data entry logged 196 | * @returns {Promise} new report data 197 | */ 198 | 199 | /** 200 | * This callback is displayed as part of the deleteTodo function. 201 | * @memberof adapters 202 | * @callback deleteTodoReturn 203 | * @param {string} id id of the current data for update 204 | * @param {string} owner of the data entry logged 205 | * @returns {Promise} new report data 206 | */ 207 | 208 | /** 209 | * This callback is displayed as part of the getTodo function. 210 | * @memberof adapters 211 | * @callback getTodoReturn 212 | * @param {string} id key of the data 213 | * @returns {Promise} task from repository 214 | */ 215 | -------------------------------------------------------------------------------- /src/adapters/todo.spec.js: -------------------------------------------------------------------------------- 1 | import { getDocument, putDocument, updateDocument, deleteDocument } from '../ports/state-machines/aws.dynamo' 2 | import todoAdapterFactory from './todo' 3 | import { ETodoStatus, EPriority } from '../business/constants' 4 | import { validateUpdateTodo } from '../business/todo' 5 | import moment from 'moment' 6 | import R from 'ramda' 7 | import { v4 as uuidv4 } from 'uuid' 8 | import { EClassError } from '../utils' 9 | import { throwCustomError } from '../utils/errors' 10 | 11 | /** mock error generation to validate signature */ 12 | jest.mock('../utils/errors') 13 | 14 | throwCustomError.mockImplementation((error) => { 15 | throw error 16 | }) 17 | 18 | // this adapter will mock all methods from aws.dynamo port 19 | jest.mock('../ports/state-machines/aws.dynamo') 20 | 21 | // mock escriba calls 22 | const escribaMock = { 23 | info: jest.fn((args) => (args)).mockReturnValue(undefined) 24 | } 25 | 26 | // mock repository structure to test your elements 27 | const repositoryMock = { 28 | getDocument, 29 | putDocument, 30 | updateDocument, 31 | deleteDocument 32 | } 33 | 34 | // mock instantiated adapter 35 | const adapterInstiated = todoAdapterFactory(escribaMock, repositoryMock) 36 | 37 | describe('getTodo', () => { 38 | const methodPath = 'adapters.todo.getTodo' 39 | beforeEach(() => { 40 | getDocument.mockReset() 41 | }) 42 | 43 | const getDocumentMock = (args) => jest.fn().mockResolvedValue({ 44 | id: args.id, 45 | taskOrder: 0, 46 | taskDescription: 'mocktaskDescription', 47 | taskOwner: 'owner', 48 | taskStatus: ETodoStatus.IN_PROGRESS, 49 | taskPriority: EPriority.MODERATE, 50 | creationDate: moment().toISOString(), 51 | lastUpdateDate: null 52 | }) 53 | 54 | const newId = uuidv4() 55 | 56 | test('default case', async () => { 57 | repositoryMock.getDocument.mockImplementationOnce((args) => getDocumentMock(args)()) 58 | 59 | await expect(adapterInstiated.getTodo(newId)) 60 | .resolves.toMatchObject({ 61 | id: newId, 62 | taskOrder: 0, 63 | taskDescription: 'mocktaskDescription', 64 | taskOwner: 'owner', 65 | taskStatus: ETodoStatus.IN_PROGRESS, 66 | taskPriority: EPriority.MODERATE 67 | }) 68 | expect(getDocument).toHaveBeenCalled() 69 | expect(getDocument).toHaveBeenLastCalledWith({ id: newId }) 70 | }) 71 | 72 | test('throw error', async () => { 73 | const throwMessage = 'invalid id' 74 | const getDocumentErrorMock = (args) => jest.fn().mockRejectedValue(new Error(throwMessage)) 75 | repositoryMock.getDocument.mockImplementationOnce((args) => getDocumentErrorMock(args)()) 76 | await expect(adapterInstiated.getTodo(newId)).rejects.toThrow(throwMessage) 77 | // throws correct message 78 | expect(throwCustomError).toHaveBeenCalledWith(new Error(throwMessage), methodPath, EClassError.INTERNAL) 79 | expect(getDocument).toHaveBeenCalled() 80 | expect(getDocument).toHaveBeenLastCalledWith({ id: newId }) 81 | }) 82 | }) 83 | 84 | describe('createTodo', () => { 85 | const methodPath = 'adapters.todo.createTodo' 86 | beforeEach(() => { 87 | putDocument.mockReset() 88 | }) 89 | 90 | const putDocumentMock = (args) => jest.fn().mockResolvedValue(args) 91 | 92 | const newData = { 93 | taskOrder: 0, 94 | taskDescription: 'testDescription', 95 | taskPriority: EPriority.HIGH 96 | } 97 | 98 | test('default case', async () => { 99 | repositoryMock.putDocument.mockImplementationOnce((args) => putDocumentMock(args)()) 100 | const insertedData = await adapterInstiated.createTodo(newData, 'owner') 101 | 102 | expect(insertedData).toMatchObject({ 103 | ...newData, 104 | taskStatus: ETodoStatus.NEW, 105 | taskOwner: 'owner' 106 | }) 107 | expect(putDocument).toHaveBeenCalled() 108 | expect(putDocument).toHaveBeenLastCalledWith(insertedData) 109 | expect(escribaMock.info).toHaveBeenCalled() 110 | expect(escribaMock.info).toHaveBeenCalledWith({ 111 | action: 'TASK_CREATED', 112 | method: methodPath, 113 | data: { documentInserted: insertedData } 114 | }) 115 | }) 116 | 117 | test('throw error', async () => { 118 | const throwMessage = 'invalid data' 119 | const putDocumentErrorMock = (args) => jest.fn().mockRejectedValue(new Error(throwMessage)) 120 | repositoryMock.putDocument.mockImplementationOnce((args) => putDocumentErrorMock(args)()) 121 | await expect(adapterInstiated.createTodo(newData, 'owner')).rejects.toThrow(throwMessage) 122 | // throws correct message 123 | expect(throwCustomError).toHaveBeenCalledWith(new Error(throwMessage), methodPath, EClassError.INTERNAL) 124 | expect(putDocument).toHaveBeenCalled() 125 | }) 126 | 127 | test('throw error with invalid data (business validation)', async () => { 128 | repositoryMock.putDocument.mockImplementationOnce((args) => putDocumentMock(args)()) 129 | await expect(adapterInstiated.createTodo({}, 'owner')).rejects.toThrow() 130 | expect(putDocument).not.toHaveBeenCalled() 131 | }) 132 | }) 133 | 134 | describe('updateTodo', () => { 135 | const methodPath = 'adapters.todo.updateTodo' 136 | beforeEach(() => { 137 | updateDocument.mockReset() 138 | getDocument.mockReset() 139 | }) 140 | 141 | const newData = { 142 | id: uuidv4(), 143 | taskOrder: 0, 144 | taskDescription: 'testDescriptionUpdate', 145 | taskPriority: EPriority.HIGH, 146 | taskStatus: ETodoStatus.NEW, 147 | taskOwner: 'owner', 148 | creationData: moment().toISOString(), 149 | lastUpdateDate: null 150 | } 151 | 152 | const updatedData = { 153 | taskPriority: EPriority.LOW, 154 | taskStatus: ETodoStatus.IN_PROGRESS 155 | } 156 | 157 | const getDocumentMock = jest.fn().mockResolvedValue(newData) 158 | const updatedTodoMock = validateUpdateTodo(updatedData, newData, 'updateOwner') 159 | const updateDocumentMock = (key, updateExpression, expressionAttributeValues) => jest.fn().mockResolvedValue(updatedTodoMock) 160 | 161 | test('default case', async () => { 162 | repositoryMock.updateDocument.mockImplementationOnce((key, updateExpression, expressionAttributeValues) => updateDocumentMock(key, updateExpression, expressionAttributeValues)()) 163 | repositoryMock.getDocument.mockImplementationOnce(getDocumentMock) 164 | const updatedTodo = await adapterInstiated.updateTodo(newData.id, updatedData, 'updateOwner') 165 | expect(updatedTodo).toMatchObject(updatedTodoMock) 166 | const updateExpression = ` 167 | set taskOrder = :taskOrder, 168 | taskDescription = :taskDescription, 169 | taskStatus = :taskStatus, 170 | taskPriority = :taskPriority, 171 | lastUpdateDate = :lastUpdateDate 172 | ` 173 | expect(updateDocument).toHaveBeenCalled() 174 | expect(updateDocument).toHaveBeenCalledWith({ id: newData.id }, updateExpression, expect.objectContaining(R.dissoc('lastUpdateDate', updatedTodo))) 175 | expect(escribaMock.info).toHaveBeenCalled() 176 | expect(escribaMock.info).toHaveBeenCalledWith({ 177 | action: 'TASK_UPDATED', 178 | method: methodPath, 179 | data: updatedTodo 180 | }) 181 | }) 182 | 183 | test('throw error', async () => { 184 | const throwMessage = 'invalid data' 185 | const updateDocumentMockError = (key, updateExpression, expressionAttributeValues) => jest.fn().mockRejectedValue(new Error(throwMessage)) 186 | repositoryMock.updateDocument.mockImplementationOnce((key, updateExpression, expressionAttributeValues) => updateDocumentMockError(key, updateExpression, expressionAttributeValues)()) 187 | repositoryMock.getDocument.mockImplementationOnce(getDocumentMock) 188 | 189 | await expect(adapterInstiated.updateTodo(newData.id, updatedData, 'ownerUpdateError')).rejects.toThrow() 190 | // throws correct message 191 | expect(throwCustomError).toHaveBeenCalledWith(new Error(throwMessage), methodPath, EClassError.INTERNAL) 192 | expect(updateDocument).toHaveBeenCalled() 193 | }) 194 | 195 | test('throw error with invalid data (business validation)', async () => { 196 | repositoryMock.updateDocument.mockImplementationOnce((key, updateExpression, expressionAttributeValues) => updateDocumentMock(key, updateExpression, expressionAttributeValues)()) 197 | repositoryMock.getDocument.mockImplementationOnce(getDocumentMock) 198 | 199 | await expect(adapterInstiated.updateTodo(newData.id, {}, 'ownerUpdateErrorValidation')).rejects.toThrow() 200 | expect(updateDocument).not.toHaveBeenCalled() 201 | }) 202 | }) 203 | 204 | describe('deleteTodo', () => { 205 | const methodPath = 'adapters.todo.deleteTodo' 206 | beforeEach(() => { 207 | deleteDocument.mockReset() 208 | }) 209 | 210 | const newData = { 211 | id: uuidv4(), 212 | taskOrder: 0, 213 | taskDescription: 'testDescriptionUpdate', 214 | taskPriority: EPriority.HIGH, 215 | taskStatus: ETodoStatus.NEW, 216 | taskOwner: 'owner', 217 | creationData: moment().toISOString(), 218 | lastUpdateDate: null 219 | } 220 | 221 | const deleteDocumentMock = (args) => jest.fn().mockResolvedValue(newData) 222 | const getDocumentMock = jest.fn().mockResolvedValue(newData) 223 | 224 | test('default case', async () => { 225 | repositoryMock.deleteDocument.mockImplementationOnce((args) => deleteDocumentMock(args)()) 226 | repositoryMock.getDocument.mockImplementationOnce(getDocumentMock) 227 | const deletedTodo = await adapterInstiated.deleteTodo(newData.id, 'deleteOwner') 228 | expect(deletedTodo).toMatchObject(newData) 229 | expect(deleteDocument).toHaveBeenCalled() 230 | expect(deleteDocument).toHaveBeenCalledWith({ id: newData.id }) 231 | expect(escribaMock.info).toHaveBeenCalled() 232 | expect(escribaMock.info).toHaveBeenCalledWith({ 233 | action: 'TASK_DELETED', 234 | method: methodPath, 235 | data: deletedTodo 236 | }) 237 | }) 238 | 239 | test('throw error', async () => { 240 | const throwMessage = 'invalid id' 241 | const deleteDocumentErrorMock = (args) => jest.fn().mockRejectedValue(new Error(throwMessage)) 242 | repositoryMock.deleteDocument.mockImplementationOnce((args) => deleteDocumentErrorMock(args)()) 243 | repositoryMock.getDocument.mockImplementationOnce(getDocumentMock) 244 | 245 | await expect(adapterInstiated.deleteTodo(newData.id, 'deleteOwner')).rejects.toThrow() 246 | // throws correct message 247 | expect(throwCustomError).toHaveBeenCalledWith(new Error(throwMessage), methodPath, EClassError.INTERNAL) 248 | expect(getDocument).toHaveBeenCalled() 249 | expect(getDocument).toHaveBeenCalledWith({ id: newData.id }) 250 | }) 251 | }) 252 | -------------------------------------------------------------------------------- /src/business/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Enum for ETodoStatus values. 3 | * @readonly 4 | * @memberof business 5 | * @enum {string} 6 | */ 7 | export const ETodoStatus = { 8 | NEW: 'NEW', 9 | IN_PROGRESS: 'IN_PROGRESS', 10 | WAITING_TRANSMISSION: 'WAITING_TRANSMISSION', 11 | CLOSED: 'CLOSED', 12 | CANCELED: 'CANCELED' 13 | } 14 | 15 | /** 16 | * Enum for EPriority values. 17 | * @readonly 18 | * @memberof business 19 | * @enum {string} 20 | */ 21 | export const EPriority = { 22 | LOW: 'LOW', 23 | MODERATE: 'MODERATE', 24 | HIGH: 'HIGH', 25 | URGENT: 'URGENT' 26 | } 27 | -------------------------------------------------------------------------------- /src/business/constants.spec.js: -------------------------------------------------------------------------------- 1 | import { ETodoStatus, EPriority } from './constants' 2 | 3 | describe('constants', () => { 4 | test('ETodoStatus', () => { 5 | expect(ETodoStatus.CANCELED).toBe('CANCELED') 6 | expect(ETodoStatus.CLOSED).toBe('CLOSED') 7 | expect(ETodoStatus.IN_PROGRESS).toBe('IN_PROGRESS') 8 | expect(ETodoStatus.NEW).toBe('NEW') 9 | expect(ETodoStatus.WAITING_TRANSMISSION).toBe('WAITING_TRANSMISSION') 10 | }) 11 | test('EPriority', () => { 12 | expect(EPriority.HIGH).toBe('HIGH') 13 | expect(EPriority.LOW).toBe('LOW') 14 | expect(EPriority.MODERATE).toBe('MODERATE') 15 | expect(EPriority.URGENT).toBe('URGENT') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/business/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Business Namespace. 3 | * @namespace business 4 | * 5 | * 6 | * @description this namespace control all business control of the solution 7 | */ 8 | /** 9 | * code imports 10 | */ 11 | // eslint-disable-next-line no-unused-vars 12 | import { ETodoStatus, EPriority } from './constants' 13 | 14 | /** 15 | * @typedef {Object} TodoKey 16 | * @property {string} id id of the task 17 | */ 18 | 19 | /** 20 | * @typedef {Object} Todo 21 | * @property {string} id id of the task 22 | * @property {number} taskOrder order of activity 23 | * @property {string} taskDescription description of taks 24 | * @property {user} taskOwner of the task 25 | * @property {ETodoStatus} taskStatus status of report 26 | * @property {EPriority} taskPriority priority of report 27 | * @property {string} creationDate datetime of creation 28 | * @property {string} lastUpdateDate datetime of the last update 29 | */ 30 | 31 | /** 32 | * @typedef {Object} MutateTodoInput object to input in mutations 33 | * @property {number} taskOrder order of activity 34 | * @property {string} taskDescription description of taks 35 | * @property {ETodoStatus} taskStatus status of report 36 | * @property {EPriority} taskPriority priority of report 37 | */ 38 | 39 | /** 40 | * @typedef {Object} MutateTodoOutput object to input in mutations 41 | * @property {string} taskDescription description of taks 42 | * @property {ETodoStatus} taskStatus status of report 43 | * @property {EPriority} taskPriority priority of report 44 | * @property {string} lastUpdateDate datetime of the last update 45 | */ 46 | -------------------------------------------------------------------------------- /src/business/moment.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | import { Moment, MomentInput } from 'moment' 3 | // Code imports 4 | import R from 'ramda' 5 | import { momentConfig } from '../config' 6 | import moment from 'moment-timezone/builds/moment-timezone-with-data' 7 | 8 | import { 9 | EClassError, 10 | throwCustomError, 11 | // eslint-disable-next-line no-unused-vars 12 | CustomError 13 | } from '../utils' 14 | 15 | /** 16 | * @description Moment with timezone local 17 | * @memberof business 18 | * @function 19 | * @throws {CustomError} 20 | * @param {MomentInput} dta (optional) override dta if necessary 21 | * @param {string} timezone (optional) overload default timezone if necessary 22 | * @returns {Moment} moment with timezone configure 23 | */ 24 | const momentWithTz = (dta, timezone = momentConfig.timezone) => { 25 | if (!isValidEntry(dta)) { 26 | throwCustomError(new Error(`invalid dateTime entry, got "${dta}"`), 'business.moment.momentWithTz', EClassError.INTERNAL) 27 | } 28 | return (R.isNil(dta) ? moment() : moment(dta)).tz(timezone) 29 | } 30 | 31 | /** 32 | * @description Get the current time formated with 'YYYYMMDDHHmm' 33 | * @memberof business 34 | * @function 35 | * @param {Moment} dta instantiate moment object 36 | * @returns {string} String datetime with format. 37 | */ 38 | const getDateFormated = (dta) => { 39 | return dta.format('YYYYMMDDHHmm') 40 | } 41 | 42 | /** 43 | * @description Moment with timezone local in iso8601 44 | * @memberof business 45 | * @function 46 | * @param {Moment} dta (optional) moment instance for overload "new moment()" if necessary 47 | * @param {string} timezone (optional) overload default timezone if necessary 48 | * @returns {string} iso8601 string datetime with timezone defined 49 | */ 50 | const toISOString = (dta, timezone = momentConfig.timezone) => { 51 | return (R.isNil(dta) ? momentWithTz(null, timezone) : dta).toISOString(true) 52 | } 53 | 54 | /** 55 | * @description return if entry string is a valid iso8601 data 56 | * @memberof business 57 | * @function 58 | * @param {Moment} dta instantiate moment object 59 | * @returns {boolean} is valid? 60 | */ 61 | const isValidEntry = (dta) => { 62 | if (R.not(R.isNil(dta)) && 63 | R.not(moment(dta, moment.ISO_8601).isValid())) { 64 | return false 65 | } 66 | return true 67 | } 68 | 69 | /** 70 | * Centralizando as configurações do moment 71 | */ 72 | export { momentWithTz, toISOString, getDateFormated, isValidEntry } 73 | -------------------------------------------------------------------------------- /src/business/moment.spec.js: -------------------------------------------------------------------------------- 1 | import { getDateFormated, momentWithTz, toISOString, isValidEntry } from './moment' 2 | import { EClassError } from '../utils' 3 | import { throwCustomError } from '../utils/errors' 4 | 5 | /** mock error generation to validate signature */ 6 | jest.mock('../utils/errors') 7 | 8 | throwCustomError.mockImplementation((error) => { 9 | throw error 10 | }) 11 | 12 | describe('moment timezone', () => { 13 | const methodPath = 'business.moment.momentWithTz' 14 | test('invalid entry', () => { 15 | const localTimezone = 'America/Sao_Paulo' 16 | const testDateString = 'INVALID' 17 | 18 | expect(() => { 19 | momentWithTz(testDateString, localTimezone) 20 | }).toThrow(`invalid dateTime entry, got "${testDateString}"`) 21 | // throws correct message 22 | expect(throwCustomError).toHaveBeenCalledWith(new Error(`invalid dateTime entry, got "${testDateString}"`), methodPath, EClassError.INTERNAL) 23 | }) 24 | 25 | test('date fixed with local timezone', () => { 26 | const hourMinutes = 60 27 | const localOffset = -3 28 | const localTimezone = 'America/Sao_Paulo' 29 | const testDateString = '2020-06-01T12:00:00Z' 30 | const testDateLocal = momentWithTz(testDateString, localTimezone) 31 | 32 | expect(testDateLocal.utcOffset()).toBe(localOffset * hourMinutes) 33 | expect(testDateLocal.month()).toBe(5) 34 | expect(testDateLocal.date()).toBe(1) 35 | expect(testDateLocal.year()).toBe(2020) 36 | }) 37 | 38 | test('format date fixed with local timezone', () => { 39 | const localTimezone = 'America/Sao_Paulo' 40 | const testDateString = '2020-06-01T12:00:00Z' 41 | const testDateLocal = momentWithTz(testDateString, localTimezone) 42 | 43 | expect(getDateFormated(testDateLocal)).toBe('202006010900') 44 | }) 45 | 46 | test('date fixed with UTC timezone', () => { 47 | const config = { 48 | timezone: 'Etc/UTC' 49 | } 50 | const testDateString = '2020-06-01T12:00:00Z' 51 | 52 | const hourMinutes = 60 53 | const utcOffset = 0 54 | const testDateUTC = momentWithTz(testDateString, config.timezone) 55 | 56 | expect(testDateUTC.utcOffset()).toBe(utcOffset * hourMinutes) 57 | expect(testDateUTC.month()).toBe(5) 58 | expect(testDateUTC.date()).toBe(1) 59 | expect(testDateUTC.year()).toBe(2020) 60 | }) 61 | 62 | test('format date fixed with UTC timezone', () => { 63 | const config = { 64 | timezone: 'Etc/UTC' 65 | } 66 | const testDateString = '2020-06-01T12:00:00Z' 67 | const testDateUTC = momentWithTz(testDateString, config.timezone) 68 | 69 | expect(getDateFormated(testDateUTC)).toBe('202006011200') 70 | }) 71 | 72 | test('format date fixed with UTC timezone using isoString', () => { 73 | const config = { 74 | timezone: 'Etc/UTC' 75 | } 76 | const testDateString = '2020-06-01T12:00:00Z' 77 | const testDateUTC = momentWithTz(testDateString, config.timezone) 78 | 79 | expect(toISOString(testDateUTC)).toBe('2020-06-01T12:00:00.000+00:00') 80 | }) 81 | 82 | test('date now with local timezone', () => { 83 | const hourMinutes = 60 84 | const localOffset = -3 85 | const testDateNowConfigured = momentWithTz() 86 | 87 | /*** 88 | * disclaimer this scenario can be intermitent because the call of date now is called 2 times in differente moments in milliseconds, the precision of format is in minutes 89 | */ 90 | const testDateNowCompare = new Date() 91 | 92 | expect(testDateNowConfigured.utcOffset()).toBe(localOffset * hourMinutes) 93 | expect(testDateNowConfigured.month()).toBe(testDateNowCompare.getMonth()) 94 | expect(testDateNowConfigured.date()).toBe(testDateNowCompare.getDate()) 95 | expect(testDateNowConfigured.year()).toBe(testDateNowCompare.getFullYear()) 96 | }) 97 | 98 | test('format date now with local timezone', () => { 99 | const testDateNowConfigured = momentWithTz() 100 | /*** 101 | * disclaimer this scenario can be intermitent because the call of date now is called 2 times in differente moments in milliseconds, the precision of format is in minutes 102 | */ 103 | const testDateNowCompare = new Date() 104 | 105 | expect(getDateFormated(testDateNowConfigured)).toBe(`${testDateNowCompare.getFullYear()}${(testDateNowCompare.getMonth() + 1).toString().padStart(2, '0')}${testDateNowCompare.getDate().toString().padStart(2, '0')}${testDateNowCompare.getHours().toString().padStart(2, '0')}${testDateNowCompare.getMinutes().toString().padStart(2, '0')}`) 106 | }) 107 | 108 | test('date now with local timezone and moment with entry param', () => { 109 | const testDateNowCompare = new Date() 110 | const testDateNowConfiguredWithDateEntry = momentWithTz(testDateNowCompare) 111 | const hourMinutes = 60 112 | const localOffset = -3 113 | 114 | expect(testDateNowConfiguredWithDateEntry.utcOffset()).toBe(localOffset * hourMinutes) 115 | expect(testDateNowConfiguredWithDateEntry.month()).toBe(testDateNowCompare.getMonth()) 116 | expect(testDateNowConfiguredWithDateEntry.date()).toBe(testDateNowCompare.getDate()) 117 | expect(testDateNowConfiguredWithDateEntry.year()).toBe(testDateNowCompare.getFullYear()) 118 | }) 119 | 120 | test('format date now with local timezone and moment with entry param', () => { 121 | const testDateNowCompare = new Date() 122 | const testDateNowConfiguredWithDateEntry = momentWithTz(testDateNowCompare) 123 | 124 | expect(getDateFormated(testDateNowConfiguredWithDateEntry)).toBe(`${testDateNowCompare.getFullYear()}${(testDateNowCompare.getMonth() + 1).toString().padStart(2, '0')}${testDateNowCompare.getDate().toString().padStart(2, '0')}${testDateNowCompare.getHours().toString().padStart(2, '0')}${testDateNowCompare.getMinutes().toString().padStart(2, '0')}`) 125 | }) 126 | 127 | test('date now with UTC timezone', () => { 128 | const config = { 129 | timezone: 'Etc/UTC' 130 | } 131 | const testDateNowCompare = new Date() 132 | const testDateNowUTC = momentWithTz(null, config.timezone) 133 | const hourMinutes = 60 134 | const localOffset = 0 135 | 136 | expect(testDateNowUTC.utcOffset()).toBe(localOffset * hourMinutes) 137 | expect(testDateNowUTC.month()).toBe(testDateNowCompare.getUTCMonth()) 138 | expect(testDateNowUTC.date()).toBe(testDateNowCompare.getUTCDate()) 139 | expect(testDateNowUTC.year()).toBe(testDateNowCompare.getUTCFullYear()) 140 | }) 141 | }) 142 | 143 | describe('isValidEntry', () => { 144 | test('invalid entry', () => { 145 | expect(isValidEntry('INVALID')).toBe(false) 146 | }) 147 | 148 | test('valid entry', () => { 149 | expect(isValidEntry('2020-01-01')).toBe(true) 150 | }) 151 | }) 152 | -------------------------------------------------------------------------------- /src/business/todo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * reference only imports (for documentation) 3 | */ 4 | // eslint-disable-next-line no-unused-vars 5 | import { Todo, MutateTodoInput, MutateTodoOutput } from './index' 6 | /** 7 | * code imports 8 | */ 9 | import { v4 as uuidv4 } from 'uuid' 10 | import { toISOString } from './moment' 11 | import { ETodoStatus, EPriority } from './constants' 12 | import R from 'ramda' 13 | // import { EPriority, ETodoStatus } from './constants' 14 | import { 15 | EClassError, 16 | throwCustomError, 17 | // eslint-disable-next-line no-unused-vars 18 | CustomError 19 | } from '../utils' 20 | 21 | /** 22 | * @description Validate a Todo event on creation 23 | * @memberof business 24 | * @function 25 | * @throws {CustomError} 26 | * @param {Todo} data imput data for create task 27 | * @param {string} owner owner of the task 28 | * @returns {Todo} 29 | */ 30 | export const validateCreateTodo = (data, owner) => { 31 | const creationDate = toISOString() 32 | const methodPath = 'business.todo.validateCreateTodo' 33 | 34 | if (R.isEmpty(data) || R.isNil(data)) { 35 | throwCustomError(new Error('invalid entry on field data, missing information'), methodPath, EClassError.USER_ERROR) 36 | } 37 | 38 | if (R.isEmpty(data.taskDescription) || R.isNil(data.taskDescription)) { 39 | throwCustomError(new Error('invalid entry on field data, missing information about taskDescription'), methodPath, EClassError.USER_ERROR) 40 | } 41 | 42 | if (R.isNil(owner)) { 43 | throwCustomError(new Error('owner is missing'), methodPath, EClassError.USER_ERROR) 44 | } 45 | 46 | if ((R.not(R.isNil(data.taskPriority)) && R.not(Object.values(EPriority).includes(data.taskPriority)))) { 47 | throwCustomError(new Error(`invalid value for priority: got ${data.taskPriority}`), methodPath, EClassError.USER_ERROR) 48 | } 49 | 50 | if ((R.not(R.isNil(data.taskStatus)) && R.not(Object.values(ETodoStatus).includes(data.taskStatus)))) { 51 | throwCustomError(new Error(`invalid value for status: got ${data.taskStatus}`), methodPath, EClassError.USER_ERROR) 52 | } 53 | 54 | return { 55 | // default values if is missing 56 | taskOrder: 0, 57 | taskPriority: EPriority.LOW, 58 | taskStatus: ETodoStatus.NEW, 59 | ...data, 60 | // information from system 61 | taskOwner: owner, 62 | creationDate, 63 | id: uuidv4() 64 | } 65 | } 66 | 67 | /** 68 | * @description Validate a Todo event on update 69 | * @memberof business 70 | * @function 71 | * @throws {CustomError} 72 | * @param {MutateTodoInput} data update task input 73 | * @param {Todo} originalData current task data 74 | * @param {string} owner owner of the task 75 | * @returns {MutateTodoOutput} 76 | */ 77 | export const validateUpdateTodo = (data, originalData, owner) => { 78 | const lastUpdateDate = toISOString() 79 | const methodPath = 'business.todo.validateUpdateTodo' 80 | 81 | if (R.isNil(originalData)) { 82 | throwCustomError(new Error('no data for this id'), methodPath, EClassError.USER_ERROR) 83 | } 84 | 85 | if (R.isEmpty(data) || R.isNil(data)) { 86 | throwCustomError(new Error('invalid entry on field data, missing information'), methodPath, EClassError.USER_ERROR) 87 | } 88 | 89 | if (R.isNil(owner)) { 90 | throwCustomError(new Error('owner is missing'), methodPath, EClassError.USER_ERROR) 91 | } 92 | 93 | return ['taskOwner', 'id', 'creationDate'] 94 | .reduce( 95 | (reducedData, field) => R.dissoc(field, reducedData), 96 | { 97 | ...originalData, 98 | ...data, 99 | lastUpdateDate 100 | } 101 | ) 102 | } 103 | 104 | /** 105 | * @description Validate a Todo event on delete 106 | * @memberof business 107 | * @function 108 | * @throws {CustomError} 109 | * @param {Todo} originalData current task data 110 | * @param {string} owner owner of the task 111 | * @returns {Todo} 112 | */ 113 | export const validateDeleteTodo = (originalData, owner) => { 114 | const methodPath = 'business.todo.validateDeleteTodo' 115 | if (R.isNil(originalData)) { 116 | throwCustomError(new Error('no data for this id'), methodPath, EClassError.USER_ERROR) 117 | } 118 | 119 | if (R.isNil(owner)) { 120 | throwCustomError(new Error('owner is missing'), methodPath, EClassError.USER_ERROR) 121 | } 122 | 123 | return originalData 124 | } 125 | -------------------------------------------------------------------------------- /src/business/todo.spec.js: -------------------------------------------------------------------------------- 1 | import { EPriority, ETodoStatus } from './constants' 2 | import { validateCreateTodo, validateUpdateTodo, validateDeleteTodo } from './todo' 3 | import { EClassError } from '../utils' 4 | import { throwCustomError } from '../utils/errors' 5 | 6 | /** mock error generation to validate signature */ 7 | jest.mock('../utils/errors') 8 | 9 | throwCustomError.mockImplementation((error) => { 10 | throw error 11 | }) 12 | 13 | describe('validateCreateTodo', () => { 14 | const methodPath = 'business.todo.validateCreateTodo' 15 | const validateCaseDefault = { 16 | taskDescription: 'test' 17 | } 18 | 19 | test('validate default case', () => { 20 | expect(validateCreateTodo(validateCaseDefault, 'testUser')).toMatchObject({ 21 | ...validateCaseDefault, 22 | taskStatus: ETodoStatus.NEW, 23 | taskOwner: 'testUser', 24 | taskPriority: EPriority.LOW, 25 | taskOrder: 0 26 | }) 27 | }) 28 | 29 | const validateCasePriorityInvalid = { 30 | taskOrder: 1, 31 | taskDescription: 'test', 32 | taskPriority: 'INVALID' 33 | } 34 | 35 | test('validate invalid taskPriority', () => { 36 | const throwMessage = `invalid value for priority: got ${validateCasePriorityInvalid.taskPriority}` 37 | expect(() => { 38 | validateCreateTodo(validateCasePriorityInvalid, 'testUser') 39 | }).toThrow(throwMessage) 40 | // throws correct message 41 | expect(throwCustomError).toHaveBeenCalledWith(new Error(throwMessage), methodPath, EClassError.USER_ERROR) 42 | }) 43 | 44 | const validateCaseStatusInvalid = { 45 | taskOrder: 1, 46 | taskDescription: 'test', 47 | taskStatus: 'INVALID' 48 | } 49 | 50 | test('validate invalid taskStatus on create', () => { 51 | const throwMessage = `invalid value for status: got ${validateCaseStatusInvalid.taskStatus}` 52 | expect(() => { 53 | validateCreateTodo(validateCaseStatusInvalid, 'testUser') 54 | }).toThrow(throwMessage) 55 | // throws correct message 56 | expect(throwCustomError).toHaveBeenCalledWith(new Error(throwMessage), methodPath, EClassError.USER_ERROR) 57 | }) 58 | 59 | const validateNullDescription = { 60 | taskOrder: 1 61 | } 62 | 63 | test('validate null description on create', () => { 64 | const throwMessage = 'invalid entry on field data, missing information about taskDescription' 65 | expect(() => { 66 | validateCreateTodo(validateNullDescription, 'testUser') 67 | }).toThrow(throwMessage) 68 | // throws correct message 69 | expect(throwCustomError).toHaveBeenCalledWith(new Error(throwMessage), methodPath, EClassError.USER_ERROR) 70 | }) 71 | 72 | const validateNullData = null 73 | 74 | test('validate null data on create', () => { 75 | const throwMessage = 'invalid entry on field data, missing information' 76 | expect(() => { 77 | validateCreateTodo(validateNullData, 'testUser') 78 | }).toThrow(throwMessage) 79 | // throws correct message 80 | expect(throwCustomError).toHaveBeenCalledWith(new Error(throwMessage), methodPath, EClassError.USER_ERROR) 81 | }) 82 | 83 | test('validate null user on create', () => { 84 | const throwMessage = 'owner is missing' 85 | expect(() => { 86 | validateCreateTodo(validateCaseDefault) 87 | }).toThrow(throwMessage) 88 | // throws correct message 89 | expect(throwCustomError).toHaveBeenCalledWith(new Error(throwMessage), methodPath, EClassError.USER_ERROR) 90 | }) 91 | }) 92 | 93 | describe('validateUpdateTodo', () => { 94 | const methodPath = 'business.todo.validateUpdateTodo' 95 | const defaultOriginalData = validateCreateTodo({ 96 | taskPriority: EPriority.HIGH, 97 | taskDescription: 'updateDefault' 98 | }, 'owner') 99 | 100 | const validateCaseDefaultUpdate = { 101 | ...defaultOriginalData, 102 | taskStatus: ETodoStatus.IN_PROGRESS 103 | } 104 | 105 | test('validate null user on update', () => { 106 | const throwMessage = 'owner is missing' 107 | expect(() => { 108 | validateUpdateTodo(validateCaseDefaultUpdate, defaultOriginalData) 109 | }).toThrow(throwMessage) 110 | // throws correct message 111 | expect(throwCustomError).toHaveBeenCalledWith(new Error(throwMessage), methodPath, EClassError.USER_ERROR) 112 | }) 113 | 114 | test('validate null originalData on update', () => { 115 | const throwMessage = 'no data for this id' 116 | expect(() => { 117 | validateUpdateTodo(validateCaseDefaultUpdate) 118 | }).toThrow(throwMessage) 119 | // throws correct message 120 | expect(throwCustomError).toHaveBeenCalledWith(new Error(throwMessage), methodPath, EClassError.USER_ERROR) 121 | }) 122 | 123 | test('validate data when is null for update', () => { 124 | const throwMessage = 'invalid entry on field data, missing information' 125 | expect(() => { 126 | validateUpdateTodo(null, defaultOriginalData, 'testUser') 127 | }).toThrow(throwMessage) 128 | // throws correct message 129 | expect(throwCustomError).toHaveBeenCalledWith(new Error(throwMessage), methodPath, EClassError.USER_ERROR) 130 | }) 131 | 132 | test('validate normal update', () => { 133 | const validateCaseNormal = { 134 | ...defaultOriginalData, 135 | taskDescription: 'new description', 136 | taskStatus: ETodoStatus.IN_PROGRESS, 137 | taskPriority: EPriority.MODERATE 138 | } 139 | const updatedData = validateUpdateTodo(validateCaseNormal, defaultOriginalData, 'testUser') 140 | expect(updatedData) 141 | .toMatchObject({ 142 | taskDescription: 'new description', 143 | taskStatus: ETodoStatus.IN_PROGRESS, 144 | taskPriority: EPriority.MODERATE 145 | }) 146 | 147 | expect(updatedData.lastUpdateDate) 148 | .not.toBe(null) 149 | expect(updatedData) 150 | .not.toHaveProperty('taskOwner') 151 | expect(updatedData) 152 | .not.toHaveProperty('id') 153 | }) 154 | }) 155 | 156 | describe('validateDeleteTodo', () => { 157 | const methodPath = 'business.todo.validateDeleteTodo' 158 | const defaultOriginalData = validateCreateTodo({ 159 | taskPriority: EPriority.HIGH, 160 | taskDescription: 'deleteDefault' 161 | }, 'owner') 162 | 163 | test('validate null user on delete', () => { 164 | const throwMessage = 'owner is missing' 165 | expect(() => { 166 | validateDeleteTodo(defaultOriginalData) 167 | }).toThrow(throwMessage) 168 | // throws correct message 169 | expect(throwCustomError).toHaveBeenCalledWith(new Error(throwMessage), methodPath, EClassError.USER_ERROR) 170 | }) 171 | 172 | test('validate null originalData on update', () => { 173 | const throwMessage = 'no data for this id' 174 | expect(() => { 175 | validateDeleteTodo(null, 'deleteUser') 176 | }).toThrow(throwMessage) 177 | // throws correct message 178 | expect(throwCustomError).toHaveBeenCalledWith(new Error(throwMessage), methodPath, EClassError.USER_ERROR) 179 | }) 180 | 181 | test('validate normal delete', () => { 182 | expect(validateDeleteTodo(defaultOriginalData, 'testUser')) 183 | .toMatchObject(defaultOriginalData) 184 | }) 185 | }) 186 | -------------------------------------------------------------------------------- /src/config/environments.js: -------------------------------------------------------------------------------- 1 | /** 2 | * get environment variable 3 | * @memberof config 4 | * @param {string} env environment variable name 5 | * @param {string} [defaultValue=''] default value 6 | * @returns {string} environment variable value 7 | */ 8 | export const getEnv = (env, defaultValue = '') => process.env[env] || defaultValue 9 | -------------------------------------------------------------------------------- /src/config/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * config Namespace. 3 | * @namespace config 4 | * 5 | * 6 | * @description this namespace is a configuration of the project 7 | */ 8 | // eslint-disable-next-line no-unused-vars 9 | import { Configuration as Log4jsConf } from 'log4js' 10 | 11 | // code imports 12 | import { config } from 'dotenv' 13 | import R from 'ramda' 14 | import { getEnv } from './environments' 15 | config() 16 | 17 | /** 18 | * general aws configuration 19 | * @memberof config 20 | */ 21 | const AWSConfig = { 22 | accessKeyId: getEnv('AWS_ACCESS_KEY_ID'), 23 | secretAccessKey: getEnv('AWS_ACCESS_SECRET_KEY'), 24 | region: getEnv('AWS_REGION'), 25 | profile: getEnv('AWS_PROFILE') 26 | } 27 | 28 | /** 29 | * aws dynamodb configuration 30 | * @memberof config 31 | */ 32 | const AWSDynamoConfig = R.merge( 33 | AWSConfig, 34 | { 35 | region: getEnv('AWS_DYNAMO_REGION'), 36 | apiVersion: getEnv('AWS_DYNAMO_APIVERSION', '2012-08-10'), 37 | endpoint: getEnv('AWS_DYNAMO_ENDPOINT') 38 | } 39 | ) 40 | 41 | /** 42 | * aws sqs configuration 43 | * @memberof config 44 | */ 45 | const AWSSqsConfig = R.merge( 46 | AWSConfig, 47 | { 48 | region: getEnv('AWS_SQS_REGION', 'us-west-2'), 49 | apiVersion: getEnv('AWS_SQS_APIVERSION', '2012-11-05') 50 | } 51 | ) 52 | 53 | /** 54 | * aws s3 configuration 55 | * @memberof config 56 | */ 57 | const AWSS3Config = R.merge( 58 | AWSConfig, 59 | { 60 | region: getEnv('AWS_SQS_REGION', 'us-west-2'), 61 | apiVersion: getEnv('AWS_S3_APIVERSION', '2006-03-01') 62 | } 63 | ) 64 | 65 | /** 66 | * moment configuration 67 | * @memberof config 68 | */ 69 | const momentConfig = { 70 | timezone: getEnv('TIMEZONE', 'America/Sao_Paulo') 71 | } 72 | 73 | const envProdName = 'production' 74 | 75 | /** 76 | * general application configuration 77 | * @memberof config 78 | */ 79 | const appConfig = { 80 | appName: getEnv('APP_NAME', 'hexagonal-boilerplate'), 81 | isProduction: getEnv('NODE_ENV') === envProdName, 82 | envName: getEnv('NODE_ENV'), 83 | todo: { 84 | tableName: getEnv('AWS_DYNAMO_TODO_TABLE_NAME', 'todos'), 85 | queueUrl: getEnv('AWS_SQS_TODO_QUEUE_NAME', 'todo') 86 | } 87 | } 88 | 89 | /** 90 | * logger configuration fixed for all jobs 91 | * @memberof config 92 | */ 93 | const escribaConf = { 94 | sensitiveConf: { 95 | password: { 96 | paths: ['message.password'], 97 | pattern: /\w.*/g, 98 | replacer: '*' 99 | } 100 | }, 101 | log4jsConf: { 102 | appenders: { 103 | out: { 104 | type: 'console', 105 | layout: { 106 | type: 'pattern', 107 | pattern: '[%d] %m' 108 | } 109 | } 110 | }, 111 | categories: { 112 | default: { 113 | appenders: [ 114 | 'out' 115 | ], 116 | level: 'info' 117 | } 118 | } 119 | } 120 | } 121 | 122 | export { 123 | appConfig, 124 | AWSConfig, 125 | AWSDynamoConfig, 126 | AWSS3Config, 127 | AWSSqsConfig, 128 | escribaConf, 129 | envProdName, 130 | momentConfig 131 | } 132 | -------------------------------------------------------------------------------- /src/config/index.spec.js: -------------------------------------------------------------------------------- 1 | import { AWSConfig, AWSDynamoConfig, AWSS3Config, AWSSqsConfig, appConfig, momentConfig, escribaConf, envProdName } from './index' 2 | 3 | describe('config', () => { 4 | test('AWSConfig', () => { 5 | expect(AWSConfig).toHaveProperty('accessKeyId') 6 | expect(AWSConfig).toHaveProperty('secretAccessKey') 7 | expect(AWSConfig).toHaveProperty('region') 8 | expect(AWSConfig).toHaveProperty('profile') 9 | }) 10 | test('AWSDynamoConfig', () => { 11 | expect(AWSDynamoConfig).toHaveProperty('region') 12 | expect(AWSDynamoConfig).toHaveProperty('apiVersion', '2012-08-10') 13 | expect(AWSDynamoConfig).toHaveProperty('endpoint') 14 | }) 15 | test('AWSS3Config', () => { 16 | expect(AWSS3Config).toHaveProperty('region') 17 | expect(AWSS3Config).toHaveProperty('apiVersion', '2006-03-01') 18 | }) 19 | test('AWSSqsConfig', () => { 20 | expect(AWSSqsConfig).toHaveProperty('region') 21 | expect(AWSSqsConfig).toHaveProperty('apiVersion', '2012-11-05') 22 | }) 23 | test('momentConfig', () => { 24 | expect(momentConfig).toHaveProperty('timezone', 'America/Sao_Paulo') 25 | }) 26 | test('envProdName', () => { 27 | expect(envProdName).toBe('production') 28 | }) 29 | test('appConfig', () => { 30 | expect(appConfig).toHaveProperty('appName', 'hexagonal-boilerplate') 31 | expect(appConfig).toHaveProperty('isProduction', false) 32 | expect(appConfig.isProduction).not.toBeUndefined() 33 | expect(appConfig.isProduction).not.toBeNull() 34 | expect(appConfig).toHaveProperty('envName', 'test') 35 | expect(appConfig).toHaveProperty('todo') 36 | expect(appConfig.todo).toHaveProperty('tableName', 'todos') 37 | expect(appConfig.todo).toHaveProperty('queueUrl', 'todo') 38 | }) 39 | test('escribaConf', () => { 40 | expect(escribaConf).toHaveProperty('log4jsConf') 41 | expect(escribaConf.log4jsConf).toHaveProperty('appenders') 42 | expect(escribaConf.log4jsConf.appenders).toHaveProperty('out', { 43 | type: 'console', 44 | layout: { 45 | type: 'pattern', 46 | pattern: '[%d] %m' 47 | } 48 | }) 49 | expect(escribaConf.log4jsConf.categories).toHaveProperty('default', { 50 | appenders: [ 51 | 'out' 52 | ], 53 | level: 'info' 54 | }) 55 | expect(escribaConf).toHaveProperty('sensitiveConf') 56 | expect(escribaConf.sensitiveConf).toHaveProperty('password', { 57 | paths: ['message.password'], 58 | pattern: /\w.*/g, 59 | replacer: '*' 60 | }) 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /src/ports/aws-lambda/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * aws lambda ports Namespace. 3 | * @namespace ports/aws/lambda 4 | * 5 | * 6 | * @description this namespace is lambda handler for aws service functions 7 | */ 8 | -------------------------------------------------------------------------------- /src/ports/aws-lambda/todo.js: -------------------------------------------------------------------------------- 1 | import AWS from 'aws-sdk' 2 | 3 | import { appConfig, AWSDynamoConfig } from '../../config' 4 | import { adapter } from '../../adapters' 5 | import { handleLogger } from '../logger/logger' 6 | import { databaseRepository } from '../state-machines' 7 | 8 | /** 9 | * Todo handler. 10 | * more about: https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html 11 | * 12 | * @memberof ports/aws/lambda 13 | * @param {*} event event object information from lambda (https://docs.aws.amazon.com/pt_br/lambda/latest/dg/with-s3.html) 14 | * @param {*} context information from direct call with params 15 | * @param {*} circuit breaker function 16 | */ 17 | export const handler = async (event, context) => { 18 | const appName = 'todo' 19 | const isProduction = process.env.ENV_NAME === 'production' 20 | const envName = isProduction ? 'production' : 'staging' 21 | 22 | // Escriba configuration. 23 | const escriba = handleLogger(appName, envName) 24 | 25 | // AWS Dynamo configuration. 26 | AWS.config.update(AWSDynamoConfig) 27 | const dynamo = new AWS.DynamoDB.DocumentClient() 28 | 29 | // inject repositories 30 | const databaseRepoInstance = databaseRepository(dynamo, appConfig.todo.tableName) 31 | const adapterInstance = adapter(escriba, databaseRepoInstance, null) 32 | 33 | const getTodo = async () => { 34 | try { 35 | const { id } = event.arguments 36 | const result = await adapterInstance.todo.getTodo(id) 37 | escriba.info('handler.get', `Get the task: ${id}`) 38 | return result 39 | } catch (error) { 40 | escriba.error('handler.generate', { ...error }) 41 | throw error 42 | } 43 | } 44 | 45 | const createTodo = async () => { 46 | try { 47 | const { user } = event.arguments 48 | const result = await adapterInstance.todo.createTodo(event.arguments.data, user) 49 | escriba.info('handler.generate', `Generated the task: ${result.id}`, result) 50 | return result 51 | } catch (error) { 52 | escriba.error('handler.generate', { ...error }) 53 | throw error 54 | } 55 | } 56 | 57 | const updateTodo = async () => { 58 | try { 59 | const { id, user } = event.arguments 60 | const result = await adapterInstance.todo.updateTodo(id, event.arguments.data, user) 61 | escriba.info('handler.generate', `Generated the task: ${result.id}`, result) 62 | return result 63 | } catch (error) { 64 | escriba.error('handler.generate', { ...error }) 65 | throw error 66 | } 67 | } 68 | 69 | const deleteTodo = async () => { 70 | try { 71 | const { id } = event.arguments 72 | const result = await adapterInstance.todo.deleteTodo(id) 73 | escriba.info('handler.get', `Delete the task: ${id}`) 74 | return result 75 | } catch (error) { 76 | escriba.error('handler.generate', { ...error }) 77 | throw error 78 | } 79 | } 80 | 81 | switch (event.field) { 82 | case 'getTodo': 83 | return getTodo() 84 | case 'createTodo': 85 | return createTodo() 86 | case 'updateTodo': 87 | return updateTodo() 88 | case 'deleteTodo': 89 | return deleteTodo() 90 | default: 91 | return getTodo() 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/ports/http/bin/www.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | import { app } from '../index' 5 | import http from 'http' 6 | const debug = require('debug')('api-server') 7 | 8 | /** 9 | * @description Get the start message default with the port. 10 | * 11 | * @memberof http 12 | * @param {number} port 13 | * @returns {string} 14 | */ 15 | const startMessageDefault = (port) => { 16 | return `The magic is on port ${port}` 17 | } 18 | 19 | /** 20 | * Event listener for HTTP server "listening" event. 21 | * 22 | * @memberof http 23 | * @returns {undefined} 24 | */ 25 | const onListening = () => { 26 | const addr = server.address() 27 | const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port 28 | debug('Listening on ' + bind) 29 | } 30 | 31 | /** 32 | * @description Normalize a port into a number, string, or false. 33 | * 34 | * @memberof http 35 | * @param {string} value Port number as string type. 36 | * @returns {number} 37 | */ 38 | const normalizePort = (value) => { 39 | const port = parseInt(value, 10) 40 | return isNaN(port) ? value : port >= 0 ? port : false 41 | } 42 | 43 | /** 44 | * Event listener for HTTP server "error" event. 45 | * @memberof http 46 | * @param {Error} error instantiated 47 | */ 48 | const onError = (error) => { 49 | if (error.syscall !== 'listen') throw error 50 | 51 | const bind = `${typeof port === 'string' ? 'Pipe' : 'Port'} ${port}` 52 | 53 | // handle specific listen errors with friendly messages 54 | switch (error.code) { 55 | case 'EACCES': 56 | console.error(bind + ' requires elevated privileges') 57 | process.exit(1) 58 | case 'EADDRINUSE': 59 | console.error(bind + ' is already in use') 60 | process.exit(1) 61 | default: 62 | throw error 63 | } 64 | } 65 | 66 | /** 67 | * Get port from environment and store in Express. 68 | * @memberof http 69 | */ 70 | const port = normalizePort(process.env.PORT || '3000') 71 | console.log(`${process.env.START_MESSAGE || startMessageDefault(port)}`) 72 | app.set('port', port) 73 | 74 | /** 75 | * Create HTTP server. 76 | * @memberof http 77 | */ 78 | const server = http.createServer(app) 79 | 80 | /** 81 | * Listen on provided port, on all network interfaces. 82 | */ 83 | server.listen(port) 84 | server.on('error', onError) 85 | server.on('listening', onListening) 86 | -------------------------------------------------------------------------------- /src/ports/http/controllers/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * controller http ports Namespace. 3 | * @namespace ports/http/controllers 4 | * 5 | * 6 | * @description this namespace is part of port http (controller section) 7 | */ 8 | // eslint-disable-next-line no-unused-vars 9 | import { Request, Response, NextFunction } from 'express' 10 | 11 | export { createTodo, deleteTodo, getTodo, updateTodo } from './todo.controller' 12 | 13 | /** 14 | * Complex callbacks documentation. 15 | * 16 | */ 17 | 18 | /** 19 | * This callback is displayed as part of the controllers function. 20 | * 21 | * @memberof ports/http/controllers 22 | * @callback ControllerTodoReturn 23 | * @param {Request} request from api in express port 24 | * @param {Response} _res response to send to caller 25 | * @param {NextFunction} next method to call in middlewares architecture 26 | * @returns {Promise} Report. 27 | */ 28 | -------------------------------------------------------------------------------- /src/ports/http/controllers/todo.controller.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | import { Logger } from 'log4js' 3 | // eslint-disable-next-line no-unused-vars 4 | import { Request, Response, NextFunction } from 'express' 5 | // eslint-disable-next-line no-unused-vars 6 | import { Todo } from '../../../business' 7 | // eslint-disable-next-line no-unused-vars 8 | import { Adapter } from '../../../adapters' 9 | // eslint-disable-next-line no-unused-vars 10 | import { ControllerTodoReturn } from './index' 11 | 12 | /** 13 | * @description Get Task by id 14 | * 15 | * @memberof ports/http/controllers 16 | * @param {Logger} escriba instance of escriba 17 | * @param {Adapter} adapter adapter instantiated 18 | * @returns {ControllerTodoReturn} 19 | */ 20 | export const getTodo = (escriba, adapter) => async (req, _res, _next) => { 21 | try { 22 | /** 23 | * disclaimer : the user in production environment, 24 | * user will be sent by the midlleware authentication who call the method on http 25 | */ 26 | const todo = await adapter.todo.getTodo(req.params.id) 27 | return todo 28 | } catch (error) { 29 | escriba.error('api.controller.todo.getTodo', error) 30 | throw error 31 | } 32 | } 33 | 34 | /** 35 | * @description Create Task 36 | * 37 | * @memberof ports/http/controllers 38 | * @param {Logger} escriba instance of escriba 39 | * @param {Adapter} adapter adapter instantiated 40 | * @returns {ControllerTodoReturn} 41 | */ 42 | export const createTodo = (escriba, adapter) => async (req, _res, _next) => { 43 | try { 44 | /** 45 | * TODO validate body 46 | */ 47 | 48 | /** 49 | * disclaimer : the user in production environment, 50 | * user will be sent by the midlleware authentication who call the method on http 51 | */ 52 | const todo = await adapter.todo.createTodo(req.body.data, req.body.user) 53 | return todo 54 | } catch (error) { 55 | escriba.error('api.controller.todo.createTodo', error) 56 | throw error 57 | } 58 | } 59 | 60 | /** 61 | * @description Update Task 62 | * 63 | * @memberof ports/http/controllers 64 | * @param {Logger} escriba instance of escriba 65 | * @param {Adapter} adapter adapter instantiated 66 | * @returns {ControllerTodoReturn} 67 | */ 68 | export const updateTodo = (escriba, adapter) => async (req, _res, _next) => { 69 | try { 70 | /** 71 | * TODO validate body 72 | */ 73 | 74 | /** 75 | * disclaimer : the user in production environment, 76 | * user will be sent by the midlleware authentication who call the method on http 77 | */ 78 | const todo = await adapter.todo.updateTodo(req.params.id, req.body.data, req.body.user) 79 | return todo 80 | } catch (error) { 81 | escriba.error('api.controller.todo.updateTodo', error) 82 | throw error 83 | } 84 | } 85 | 86 | /** 87 | * @description Delete Task 88 | * 89 | * @memberof ports/http/controllers 90 | * @param {Logger} escriba instance of escriba 91 | * @param {Adapter} adapter adapter instantiated 92 | * @returns {controllerTodoReturn} 93 | */ 94 | export const deleteTodo = (escriba, adapter) => async (req, _res, _next) => { 95 | try { 96 | /** 97 | * disclaimer : the user in production environment, 98 | * user will be sent by the midlleware authentication who call the method on http 99 | */ 100 | const todo = await adapter.todo.deleteTodo(req.params.id, req.body.user) 101 | return todo 102 | } catch (error) { 103 | escriba.error('api.controller.todo.deleteTodo', error) 104 | throw error 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/ports/http/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * route http ports Namespace. 3 | * @namespace ports/http 4 | * 5 | * 6 | * @description this namespace is part of port http 7 | */ 8 | 9 | import express from 'express' 10 | import bodyParser from 'body-parser' 11 | import { config as AWSConfig, DynamoDB } from 'aws-sdk' 12 | import { databaseRepository } from '../state-machines' 13 | import { adapter } from '../../adapters' 14 | import { appConfig, AWSDynamoConfig } from '../../config' 15 | import { getRoutes } from './routes/index' 16 | import { handleLogger } from '../logger' 17 | 18 | // Setting app 19 | const _app = express() 20 | 21 | // Escriba 22 | const escriba = handleLogger(appConfig.appName, appConfig.envName) 23 | 24 | // AWS Dynamo configuration. 25 | AWSConfig.update(AWSDynamoConfig) 26 | const dynamo = new DynamoDB.DocumentClient() 27 | 28 | // inject repositories 29 | const databaseRepoInstance = databaseRepository(dynamo, appConfig.todo.tableName) 30 | const adapterInstance = adapter(escriba, databaseRepoInstance) 31 | 32 | _app.use(bodyParser.json({ limit: '50mb' })) 33 | _app.use(bodyParser.urlencoded({ extended: false })) 34 | 35 | export const app = getRoutes(escriba, adapterInstance, _app) 36 | -------------------------------------------------------------------------------- /src/ports/http/routes/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * route http ports Namespace. 3 | * @namespace ports/http/routes 4 | * 5 | * 6 | * @description this namespace is part of port http 7 | */ 8 | 9 | /** 10 | * Reference only imports (for documentation). 11 | */ 12 | // eslint-disable-next-line no-unused-vars 13 | import { AdapterInstance } from '../../../adapters' 14 | // eslint-disable-next-line no-unused-vars 15 | import { Express } from 'express' 16 | /** 17 | * Code imports. 18 | */ 19 | import { todoRouter } from './todo.router' 20 | 21 | /** 22 | * @description Get route definitions. 23 | * 24 | * @memberof ports/http/routes 25 | * @function 26 | * @param {Logger} escriba instance of escriba 27 | * @param {AdapterInstance} adapter instantiated adapter 28 | * @param {Express} appp instantiated application express 29 | * @returns {getRoutesReturn} 30 | */ 31 | export const getRoutes = (escriba, adapter, app) => { 32 | // Route todos 33 | app.use('/api/v1/todos', todoRouter(escriba, adapter)) 34 | 35 | return app 36 | } 37 | 38 | /** 39 | * This callback is displayed as part of the getRoutes function. 40 | * 41 | * @memberof ports/http/routes 42 | * @callback getRoutesReturn 43 | * @param {Express} app - instance of express application 44 | * @returns {Express} express application with routes injected 45 | */ 46 | -------------------------------------------------------------------------------- /src/ports/http/routes/todo.router.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Reference only imports (for documentation). 3 | */ 4 | // eslint-disable-next-line no-unused-vars 5 | import { Logger } from 'log4js' 6 | // eslint-disable-next-line no-unused-vars 7 | import { Adapter } from '../../../adapters/index' 8 | 9 | /** 10 | * Code imports. 11 | */ 12 | import { Router } from 'express' 13 | import { response } from './utils' 14 | import { createTodo, deleteTodo, getTodo, updateTodo } from '../controllers/todo.controller' 15 | 16 | const router = Router() 17 | 18 | /** 19 | * @description Define the todo routes. 20 | * 21 | * @memberof ports/http/routes 22 | * @function 23 | * @param {Logger} escriba instance of escriba 24 | * @param {Adapter} adapter instantiated adapter 25 | * @returns {Router} 26 | */ 27 | 28 | export const todoRouter = (escriba, adapter) => { 29 | /** 30 | * get task with existing id 31 | */ 32 | router.get('/:id', (req, res, next) => response(getTodo(escriba, adapter)(req, res, next), res, next)) 33 | 34 | /** 35 | * create task with existing id 36 | */ 37 | router.post('/', (req, res, next) => response(createTodo(escriba, adapter)(req, res, next), res, next)) 38 | 39 | /** 40 | * update task with existing id 41 | */ 42 | router.put('/:id', (req, res, next) => response(updateTodo(escriba, adapter)(req, res, next), res, next)) 43 | 44 | /** 45 | * delete task with existing id 46 | */ 47 | router.delete('/:id', (req, res, next) => response(deleteTodo(escriba, adapter)(req, res, next), res, next)) 48 | 49 | return router 50 | } 51 | -------------------------------------------------------------------------------- /src/ports/http/routes/utils.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | import { Response, NextFunction } from 'express' 3 | 4 | /** 5 | * code imports 6 | */ 7 | import { CustomError, EClassError } from '../../../utils' 8 | 9 | /** 10 | * @description Process response as promise 11 | * 12 | * @memberof ports/http/routes 13 | * @async 14 | * @function 15 | * @throws {CustomError} 16 | * @param {Promise} prom Promise to resolve 17 | * @param {Response} res Response from request 18 | * @param {NextFunction} res Response from request 19 | * @returns {Promise} 20 | */ 21 | export const response = async (prom, res, next) => { 22 | try { 23 | const result = await prom 24 | res.status(200).json(result) 25 | } catch (error) { 26 | switch (error.constructor) { 27 | case CustomError: 28 | switch (error.internalName) { 29 | case EClassError.INTERNAL: 30 | return res.status(500).json({ ...error }) 31 | case EClassError.USER_ERROR: 32 | return res.status(400).json({ ...error }) 33 | default: 34 | return res.status(500).json({ ...error }) 35 | } 36 | default: 37 | return res.status(500).json(error.message || error) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/ports/logger/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * logger ports Namespace. 3 | * @namespace ports/logger 4 | * 5 | * 6 | * @description this namespace is a Logger shipper for application 7 | */ 8 | 9 | import { handleLogger } from './logger' 10 | 11 | export { handleLogger } 12 | -------------------------------------------------------------------------------- /src/ports/logger/logger.js: -------------------------------------------------------------------------------- 1 | import cuid from 'cuid' 2 | import escriba from 'escriba' 3 | // eslint-disable-next-line no-unused-vars 4 | import { configure as log4jsConfigure, Log4js } from 'log4js' 5 | 6 | import { escribaConf } from '../../config' 7 | 8 | /** 9 | * xDevel microservices - escriba functions 10 | * 11 | * the objetive of this js module is 12 | * 13 | * - easy to test 14 | * - easy to inject mock in more than one level 15 | * - immutability and functional programming patterns 16 | */ 17 | 18 | /** 19 | * zero config log4js for non-default configuration and bootstrap log4jsInstance 20 | * 21 | * @memberof ports/logger 22 | * @param {Object} log4jsConf 23 | * @returns {*} 24 | */ 25 | const log4jsConfigured = (log4jsConf) => log4jsConfigure(log4jsConf).getLogger 26 | 27 | /** 28 | * instantiate logger elements for middleware 29 | * 30 | * @memberof ports/logger 31 | * @param {Log4js} log4jsLogger - funciont getLogger from log4js vendor 32 | * @param {*} escribaConstructor - function for construct escriba instance 33 | * @param {*} sensitiveConf - json for sensitive properties from escriba 34 | * @param {string} appName - application name 35 | * @returns Logger 36 | */ 37 | const configureLogger = (log4jsLogger, escribaConstructor, sensitiveConf, appName) => { 38 | const escribaConfig = { 39 | loggerEngine: log4jsLogger(), 40 | service: appName, 41 | sensitive: sensitiveConf 42 | } 43 | 44 | const { logger } = escribaConstructor(escribaConfig) 45 | 46 | return logger 47 | } 48 | 49 | /** 50 | * Configure logger for all handlers. 51 | * 52 | * @memberof ports/logger 53 | * @param {string} appName - name of application 54 | * @param {string} envName - environment of the application 55 | * @returns {handleLoggerReturn} 56 | */ 57 | const handleLogger = (appName, envName) => { 58 | const logger = configureLogger(log4jsConfigured(escribaConf.log4jsConf), escriba, escribaConf.sensitiveConf, appName) 59 | const info = (method, message) => logger.info(message, { id: cuid(), from: { appName, method, envName } }) 60 | const error = (method, message) => logger.info(message, { id: cuid(), from: { appName, method, envName } }) 61 | 62 | return { 63 | logger, 64 | info, 65 | error 66 | } 67 | } 68 | 69 | export { handleLogger } 70 | 71 | /** 72 | * Complex callbacks documentation. 73 | * 74 | */ 75 | 76 | /** 77 | * This callback is displayed as part of the handleLogger function. 78 | * 79 | * @memberof ports/logger 80 | * @callback handleLoggerReturn 81 | * @param {*} logger instance of the escriba 82 | * @param {handleLoggerMessageReturn} info syntax suggar for logger.info method 83 | * @param {handleLoggerMessageReturn} error syntax suggar for logger.error method 84 | * @returns {undefined} 85 | */ 86 | 87 | /** 88 | * This callback is displayed as part of the handleLoggerReturn function. 89 | * 90 | * @memberof ports/logger 91 | * @callback handleLoggerMessageReturn 92 | * @param {LoggerFuncParams} method info for method who call the log method 93 | * @param {LoggerFuncParams} message message of the log 94 | * @returns {undefined} 95 | */ 96 | 97 | /** 98 | * @memberof ports/logger 99 | * @typedef {Object} LoggerFuncParams 100 | * @property method: string 101 | * @property message: string 102 | */ 103 | -------------------------------------------------------------------------------- /src/ports/state-machines/aws.dynamo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Reference only imports (for documentation). 3 | */ 4 | // eslint-disable-next-line no-unused-vars 5 | import { DynamoDB } from 'aws-sdk' 6 | 7 | /** 8 | * Code imports. 9 | */ 10 | import { classError } from './constants' 11 | import { throwCustomError } from '../../utils' 12 | import R from 'ramda' 13 | /** 14 | * AWS DynamoDB custom methods. 15 | */ 16 | 17 | /** 18 | * Get a document on table TableName in the DynamoDB. 19 | * 20 | * @memberof ports/state-machines 21 | * @async 22 | * @function 23 | * @throws {CustomError} 24 | * @param {DynamoDB.DocumentClient} dynamo instance of Dynamo SDK for aws (DocumentClient) 25 | * @param {string} tableName name of table in DynamoDB 26 | * @returns {getDocumentReturn} Object searched from table 27 | */ 28 | export const getDocument = (dynamo, tableName) => async (key) => { 29 | try { 30 | const params = { 31 | TableName: tableName, 32 | Key: key 33 | } 34 | 35 | const result = await dynamo.get(params).promise() 36 | 37 | return (R.not(R.isEmpty(result)) && R.not(R.isNil(result)) && R.not(R.isNil(result.Item))) ? result.Item : null 38 | } catch (error) { 39 | throwCustomError(error, 'state-machines.aws.dynamo.getDocument', classError.INTERNAL) 40 | } 41 | } 42 | 43 | /** 44 | * Insert document in the DynamoDB. 45 | * 46 | * @memberof ports/state-machines 47 | * @async 48 | * @function 49 | * @throws {CustomError} 50 | * @param {DynamoDB.DocumentClient} dynamo instance of Dynamo SDK for aws (DocumentClient) 51 | * @param {string} tableName name of table in DynamoDB 52 | * @returns {putDocumentReturn} Object searched from table 53 | */ 54 | export const putDocument = (dynamo, tableName) => async (item) => { 55 | try { 56 | const params = { 57 | TableName: tableName, 58 | Item: item 59 | } 60 | await dynamo.put(params).promise() 61 | 62 | return params.Item 63 | } catch (error) { 64 | throwCustomError(error, 'state-machines.aws.dynamo.putDocument', classError.INTERNAL) 65 | } 66 | } 67 | 68 | /** 69 | * Update document in the DynamoDB. 70 | * 71 | * @memberof ports/state-machines 72 | * @async 73 | * @function 74 | * @throws {CustomError} 75 | * @param {DynamoDB.DocumentClient} dynamo instance of Dynamo SDK for aws (DocumentClient) 76 | * @param {string} tableName name of table in DynamoDB 77 | * @returns {updateDocumentReturn} Object searched from table 78 | */ 79 | export const updateDocument = (dynamo, tableName) => async (key, updateExpression, expressionAttributeValues) => { 80 | try { 81 | const params = { 82 | TableName: tableName, 83 | Key: key, 84 | UpdateExpression: updateExpression, 85 | ExpressionAttributeValues: remapPrevixVariables(expressionAttributeValues), 86 | ReturnValues: 'ALL_NEW' 87 | } 88 | /** 89 | * @constant 90 | * @type {DynamoDB.UpdateItemOutput} 91 | */ 92 | const output = await dynamo.update(params).promise() 93 | 94 | return output.Attributes 95 | } catch (error) { 96 | throwCustomError(error, 'state-machines.aws.dynamo.updateDocument', classError.INTERNAL) 97 | } 98 | } 99 | 100 | /** 101 | * Delete a document on table TableName in the DynamoDB. 102 | * 103 | * @memberof ports/state-machines 104 | * @async 105 | * @function 106 | * @throws {CustomError} 107 | * @param {DynamoDB.DocumentClient} dynamo instance of Dynamo SDK for aws (DocumentClient) 108 | * @param {string} tableName name of table in DynamoDB 109 | * @returns {deleteDocumentReturn} Object searched from table 110 | */ 111 | export const deleteDocument = (dynamo, tableName) => async (key) => { 112 | try { 113 | const params = { 114 | TableName: tableName, 115 | Key: key, 116 | ReturnValues: 'ALL_OLD' 117 | } 118 | 119 | const result = await dynamo.delete(params).promise() 120 | 121 | return (R.not(R.isEmpty(result)) && R.not(R.isNil(result)) && R.not(R.isNil(result.Item))) ? result.Item : null 122 | } catch (error) { 123 | throwCustomError(error, 'state-machines.aws.dynamo.deleteDocument', classError.INTERNAL) 124 | } 125 | } 126 | 127 | /** 128 | * @description Add ":" in all variables in prefix remaping the object 129 | * 130 | * @memberof state-machines 131 | * @function 132 | * @param {Object} obj object param in ExpressionAttributeValues 133 | * @returns {Object} object remaped 134 | */ 135 | export const remapPrevixVariables = (obj) => { 136 | return Object 137 | .keys(obj).reduce((prev, curr) => { 138 | return { ...prev, [`:${curr}`]: obj[curr] } 139 | }, {}) 140 | } 141 | 142 | /*** 143 | * type definitions for complex objects 144 | * this helps documentation 145 | */ 146 | 147 | /** 148 | * This callback is displayed as part of the updateDocument (inner DynamoRepositoryInstance) function. 149 | * 150 | * @callback updateDocumentReturn 151 | * @param {Object} key - object of keys table parameters to search 152 | * @param {DynamoDB.UpdateExpression} updateExpression dynamo notation of the update document expression without values to change 153 | * @param {Object} expressionAttributeValues values to be mapped in updateExpression expression 154 | * @returns {Object} - object sended 155 | */ 156 | 157 | /** 158 | * This callback is displayed as part of the getDocument (inner DynamoRepositoryInstance) function. 159 | * 160 | * @callback getDocumentReturn 161 | * @param {Object} key - object of keys table parameters to search 162 | * @returns {Object} - object updated from state-machine 163 | */ 164 | 165 | /** 166 | * This callback is displayed as part of the putDocument (inner DynamoRepositoryInstance) function. 167 | * 168 | * @callback putDocumentReturn 169 | * @param {Object} item - object to persist 170 | */ 171 | 172 | /** 173 | * This callback is displayed as part of the deleteDocument (inner DynamoRepositoryInstance) function. 174 | * 175 | * @callback deleteDocumentReturn 176 | * @param {Object} key - key of the data 177 | */ 178 | -------------------------------------------------------------------------------- /src/ports/state-machines/aws.dynamo.spec.js: -------------------------------------------------------------------------------- 1 | import { DocumentClient } from 'aws-sdk/clients/dynamodb' 2 | import { databaseRepository } from './index' 3 | import { remapPrevixVariables } from './aws.dynamo' 4 | import { v4 as uuidv4 } from 'uuid' 5 | import { EClassError } from '../../utils' 6 | import { throwCustomError } from '../../utils/errors' 7 | 8 | /** 9 | * jest invocation for aws-sdk 10 | */ 11 | jest.mock('aws-sdk/clients/dynamodb') 12 | jest.mock('../../utils/errors') 13 | 14 | const dynamo = new DocumentClient() 15 | const tableName = 'mockTable' 16 | const repoInstance = databaseRepository(dynamo, 'mockTable') 17 | 18 | throwCustomError.mockImplementation((error) => { 19 | throw error 20 | }) 21 | 22 | const dynamoMockObject = { 23 | get: (Params) => jest.fn().mockReturnValue({ 24 | promise: jest.fn().mockResolvedValue({ 25 | Item: { 26 | id: Params.Key.id, 27 | description: 'mockResult' 28 | } 29 | }) 30 | }), 31 | put: (Params) => jest.fn().mockReturnValue({ 32 | promise: jest.fn().mockResolvedValue(Params.Item) 33 | }), 34 | update: (Params) => jest.fn().mockReturnValue({ 35 | promise: jest.fn().mockResolvedValue({ 36 | Attributes: { 37 | id: Params.Key.id, 38 | description: 'mockResult' 39 | } 40 | }) 41 | }), 42 | delete: (Params) => jest.fn().mockReturnValue({ 43 | promise: jest.fn().mockResolvedValue({ 44 | Item: { 45 | id: Params.Key.id, 46 | description: 'mockResult' 47 | } 48 | }) 49 | }) 50 | } 51 | 52 | describe('getDocument', () => { 53 | beforeEach(() => { 54 | DocumentClient.mockReset() 55 | }) 56 | const methodPath = 'state-machines.aws.dynamo.getDocument' 57 | test('default case', async () => { 58 | dynamo.get.mockImplementationOnce((Params) => dynamoMockObject.get(Params)()) 59 | const newId = uuidv4() 60 | 61 | await expect(repoInstance.getDocument({ id: newId })) 62 | .resolves.toMatchObject({ 63 | id: newId, 64 | description: 'mockResult' 65 | }) 66 | expect(dynamo.get).toHaveBeenCalled() 67 | expect(dynamo.get).toHaveBeenCalledWith({ Key: { id: newId }, TableName: tableName }) 68 | }) 69 | 70 | test('error', async () => { 71 | const throwMessage = 'invalid id' 72 | dynamo.get.mockImplementationOnce(jest.fn().mockReturnValue({ 73 | promise: jest.fn().mockRejectedValue(new Error(throwMessage)) 74 | })) 75 | const newId = uuidv4() 76 | await expect(repoInstance.getDocument({ id: newId })).rejects.toThrow(throwMessage) 77 | // throws correct message 78 | expect(throwCustomError).toHaveBeenCalledWith(new Error(throwMessage), methodPath, EClassError.INTERNAL) 79 | expect(dynamo.get).toHaveBeenCalled() 80 | expect(dynamo.get).toHaveBeenCalledWith({ Key: { id: newId }, TableName: tableName }) 81 | }) 82 | 83 | test('null result.Item', async () => { 84 | dynamo.get.mockImplementationOnce(jest.fn().mockReturnValue({ 85 | promise: jest.fn().mockResolvedValue({ Item: null }) 86 | })) 87 | const newId = uuidv4() 88 | 89 | await expect(repoInstance.getDocument({ id: newId })).resolves.toBe(null) 90 | expect(dynamo.get).toHaveBeenCalled() 91 | expect(dynamo.get).toHaveBeenCalledWith({ Key: { id: newId }, TableName: tableName }) 92 | }) 93 | 94 | test('null result value', async () => { 95 | dynamo.get.mockImplementationOnce(jest.fn().mockReturnValue({ 96 | promise: jest.fn().mockResolvedValue(null) 97 | })) 98 | const newId = uuidv4() 99 | 100 | await expect(repoInstance.getDocument({ id: newId })).resolves.toBe(null) 101 | expect(dynamo.get).toHaveBeenCalled() 102 | expect(dynamo.get).toHaveBeenCalledWith({ Key: { id: newId }, TableName: tableName }) 103 | }) 104 | }) 105 | 106 | describe('putDocument', () => { 107 | beforeEach(() => { 108 | DocumentClient.mockReset() 109 | }) 110 | const methodPath = 'state-machines.aws.dynamo.putDocument' 111 | test('default case', async () => { 112 | dynamo.put.mockImplementationOnce((Params) => dynamoMockObject.put(Params)()) 113 | const newId = uuidv4() 114 | 115 | await expect(repoInstance.putDocument({ 116 | id: newId, 117 | description: 'mockResult' 118 | })) 119 | .resolves.toMatchObject({ 120 | id: newId, 121 | description: 'mockResult' 122 | }) 123 | expect(dynamo.put).toHaveBeenCalled() 124 | expect(dynamo.put).toHaveBeenCalledWith({ 125 | Item: { 126 | id: newId, 127 | description: 'mockResult' 128 | }, 129 | TableName: tableName 130 | }) 131 | }) 132 | 133 | test('error', async () => { 134 | const throwMessage = 'invalid entry' 135 | dynamo.put.mockImplementationOnce(jest.fn().mockReturnValue({ 136 | promise: jest.fn().mockRejectedValue(new Error(throwMessage)) 137 | })) 138 | const newId = uuidv4() 139 | 140 | await expect(repoInstance.putDocument({ 141 | id: newId, 142 | description: 'mockResult' 143 | })).rejects.toThrow(throwMessage) 144 | // throws correct message 145 | expect(throwCustomError).toHaveBeenCalledWith(new Error(throwMessage), methodPath, EClassError.INTERNAL) 146 | expect(dynamo.put).toHaveBeenCalled() 147 | expect(dynamo.put).toHaveBeenCalledWith({ 148 | Item: { 149 | id: newId, 150 | description: 'mockResult' 151 | }, 152 | TableName: tableName 153 | }) 154 | }) 155 | }) 156 | 157 | describe('updateDocument', () => { 158 | beforeEach(() => { 159 | DocumentClient.mockReset() 160 | }) 161 | const methodPath = 'state-machines.aws.dynamo.updateDocument' 162 | test('default case', async () => { 163 | dynamo.update.mockImplementationOnce((Params) => dynamoMockObject.update(Params)()) 164 | const newId = uuidv4() 165 | 166 | await expect(repoInstance.updateDocument( 167 | { 168 | id: newId 169 | }, 170 | 'description := :description', 171 | { description: 'mockResult' } 172 | )) 173 | .resolves.toMatchObject({ 174 | id: newId, 175 | description: 'mockResult' 176 | }) 177 | expect(dynamo.update).toHaveBeenCalled() 178 | expect(dynamo.update).toHaveBeenCalledWith({ 179 | Key: { id: newId }, 180 | TableName: tableName, 181 | UpdateExpression: 'description := :description', 182 | ExpressionAttributeValues: remapPrevixVariables({ description: 'mockResult' }), 183 | ReturnValues: 'ALL_NEW' 184 | }) 185 | }) 186 | 187 | test('error', async () => { 188 | const throwMessage = 'invalid entry' 189 | dynamo.update.mockImplementationOnce(jest.fn().mockReturnValue({ 190 | promise: jest.fn().mockRejectedValue(new Error(throwMessage)) 191 | })) 192 | const newId = uuidv4() 193 | 194 | await expect(repoInstance.updateDocument( 195 | { 196 | id: newId 197 | }, 198 | 'description := :description', 199 | { description: 'mockResult' } 200 | )).rejects.toThrow(throwMessage) 201 | // throws correct message 202 | expect(throwCustomError).toHaveBeenCalledWith(new Error(throwMessage), methodPath, EClassError.INTERNAL) 203 | expect(dynamo.update).toHaveBeenCalled() 204 | expect(dynamo.update).toHaveBeenCalledWith({ 205 | Key: { id: newId }, 206 | TableName: tableName, 207 | UpdateExpression: 'description := :description', 208 | ExpressionAttributeValues: remapPrevixVariables({ description: 'mockResult' }), 209 | ReturnValues: 'ALL_NEW' 210 | }) 211 | }) 212 | }) 213 | 214 | describe('deleteDocument', () => { 215 | beforeEach(() => { 216 | DocumentClient.mockReset() 217 | }) 218 | const methodPath = 'state-machines.aws.dynamo.deleteDocument' 219 | test('default case', async () => { 220 | dynamo.delete.mockImplementationOnce((Params) => dynamoMockObject.delete(Params)()) 221 | const newId = uuidv4() 222 | 223 | await expect(repoInstance.deleteDocument({ id: newId })) 224 | .resolves.toMatchObject({ 225 | id: newId, 226 | description: 'mockResult' 227 | }) 228 | expect(dynamo.delete).toHaveBeenCalled() 229 | expect(dynamo.delete).toHaveBeenCalledWith({ Key: { id: newId }, ReturnValues: 'ALL_OLD', TableName: tableName }) 230 | }) 231 | 232 | test('error', async () => { 233 | const throwMessage = 'invalid id' 234 | dynamo.delete.mockImplementationOnce(jest.fn().mockReturnValue({ 235 | promise: jest.fn().mockRejectedValue(new Error(throwMessage)) 236 | })) 237 | const newId = uuidv4() 238 | 239 | await expect(repoInstance.deleteDocument({ id: newId })).rejects.toThrow(throwMessage) 240 | // throws correct message 241 | expect(throwCustomError).toHaveBeenCalledWith(new Error(throwMessage), methodPath, EClassError.INTERNAL) 242 | expect(dynamo.delete).toHaveBeenCalled() 243 | expect(dynamo.delete).toHaveBeenCalledWith({ Key: { id: newId }, ReturnValues: 'ALL_OLD', TableName: tableName }) 244 | }) 245 | 246 | test('null result.Item', async () => { 247 | dynamo.delete.mockImplementationOnce(jest.fn().mockReturnValue({ 248 | promise: jest.fn().mockResolvedValue({ Item: null }) 249 | })) 250 | const newId = uuidv4() 251 | 252 | await expect(repoInstance.deleteDocument({ id: newId })).resolves.toBe(null) 253 | expect(dynamo.delete).toHaveBeenCalled() 254 | expect(dynamo.delete).toHaveBeenCalledWith({ Key: { id: newId }, ReturnValues: 'ALL_OLD', TableName: tableName }) 255 | }) 256 | }) 257 | 258 | describe('remapPrevixVariables', () => { 259 | test('default case', () => { 260 | const remmaped = remapPrevixVariables({ a: 'a' }) 261 | expect(remmaped).toMatchObject({ ':a': 'a' }) 262 | }) 263 | 264 | test('empty', () => { 265 | const remmaped = remapPrevixVariables({}) 266 | expect(remmaped).toMatchObject({}) 267 | }) 268 | }) 269 | -------------------------------------------------------------------------------- /src/ports/state-machines/aws.sqs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Reference only imports (for documentation). 3 | */ 4 | // eslint-disable-next-line no-unused-vars 5 | import { SQS, AWSError } from 'aws-sdk' 6 | import R from 'ramda' 7 | /** 8 | * Code imports. 9 | */ 10 | import { classError } from './constants' 11 | import { throwCustomError } from '../../utils' 12 | 13 | /** 14 | * AWS SQS custom methods. 15 | * 16 | */ 17 | 18 | /** 19 | * @description Send the message to sqs. 20 | * @memberof ports/state-machines 21 | * @async 22 | * @function 23 | * @throws {CustomError} 24 | * @param {SQS} sqs instance of SQS sdk from aws. 25 | * @param {string} queueUrl url from sqs queue service from aws. 26 | * @returns {sendMessageReturn} 27 | */ 28 | export const sendMessage = (sqs, queueUrl) => async (body) => { 29 | const methodPath = 'ports.state-machines.aws.sqs.sendMessage' 30 | try { 31 | const params = { 32 | QueueUrl: queueUrl, 33 | MessageBody: JSON.stringify(body) 34 | } 35 | const result = await sqs.sendMessage(params).promise() 36 | 37 | if (typeof result.MessageId === 'undefined') { 38 | throw new Error('No message id response!') 39 | } 40 | 41 | return result 42 | } catch (rejectResponse) { 43 | if (rejectResponse instanceof Error) { 44 | throwCustomError(rejectResponse, methodPath, classError.INTERNAL) 45 | } else { 46 | throwCustomError(new Error(`${rejectResponse.$response.error.code}: ${rejectResponse.$response.error.message}`), methodPath, classError.INTERNAL) 47 | } 48 | } 49 | } 50 | 51 | /** 52 | * @description receive the messages from sqs. 53 | * @memberof ports/state-machines 54 | * @async 55 | * @function 56 | * @throws {CustomError} 57 | * @param {SQS} sqs instance of SQS sdk from aws. 58 | * @param {string} queueUrl url from sqs queue service from aws. 59 | * @param {number} maxNumberOfMessages max messages received from call command 60 | * @returns {receiveMessageReturn} 61 | */ 62 | export const receiveMessage = (sqs, queueUrl, maxNumberOfMessages) => async (visibilityTimeout, waitTimeSeconds) => { 63 | const methodPath = 'ports.state-machines.aws.sqs.receiveMessage' 64 | try { 65 | const messagesReceived = await sqs.receiveMessage({ 66 | QueueUrl: queueUrl, 67 | VisibilityTimeout: visibilityTimeout || 20, 68 | MaxNumberOfMessages: maxNumberOfMessages || 1, 69 | WaitTimeSeconds: waitTimeSeconds || 10 70 | }).promise() 71 | 72 | if (R.isEmpty(messagesReceived) || R.isEmpty(messagesReceived.Messages)) { 73 | throw new Error('No messages received') 74 | } 75 | 76 | return messagesReceived.Messages 77 | } catch (rejectResponse) { 78 | if (rejectResponse instanceof Error) { 79 | throwCustomError(rejectResponse, methodPath, classError.INTERNAL) 80 | } else { 81 | throwCustomError(new Error(`${rejectResponse.$response.error.code}: ${rejectResponse.$response.error.message}`), methodPath, classError.INTERNAL) 82 | } 83 | } 84 | } 85 | 86 | /** 87 | * @description delete the message from sqs queue. 88 | * @memberof ports/state-machines 89 | * @async 90 | * @function 91 | * @throws {CustomError} 92 | * @param {SQS} sqs instance of SQS sdk from aws. 93 | * @param {string} queueUrl url from sqs queue service from aws. 94 | * @returns {deleteMessageReturn} 95 | */ 96 | export const deleteMessage = (sqs, queueUrl) => async (receiptHandle) => { 97 | const methodPath = 'ports.state-machines.aws.sqs.deleteMessage' 98 | 99 | try { 100 | const params = { 101 | QueueUrl: queueUrl, 102 | ReceiptHandle: receiptHandle 103 | } 104 | const result = await sqs.deleteMessage(params).promise() 105 | 106 | return { 107 | error: result.$response.error, 108 | retryCount: result.$response.retryCount, 109 | requestId: result.$response.requestId 110 | } 111 | } catch (rejectResponse) { 112 | if (rejectResponse instanceof Error) { 113 | throwCustomError(rejectResponse, methodPath, classError.INTERNAL) 114 | } else { 115 | throwCustomError(new Error(`${rejectResponse.$response.error.code}: ${rejectResponse.$response.error.message}`), methodPath, classError.INTERNAL) 116 | } 117 | } 118 | } 119 | 120 | /** 121 | * exclusive specifications docs for complex types 122 | */ 123 | 124 | /** 125 | * @typedef {Object} DeleteMessageOutput output for deleteMessage command 126 | * @property {AWSError} error error of the call if exists 127 | * @property {number} retryCount number of retrys 128 | * @property {string} requestId id of aws request 129 | */ 130 | 131 | /** 132 | * This callback is displayed as part of the sendMessage function. 133 | * 134 | * @callback sendMessageReturn 135 | * @param {Object} body body of message 136 | * @returns {Promise} response from aws 137 | */ 138 | 139 | /** 140 | * This callback is displayed as part of the deleteMessage function. 141 | * 142 | * @callback deleteMessageReturn 143 | * @param {string} receiptHandle handle of the message for identify in sqs system 144 | * @returns {Promise} response from aws 145 | */ 146 | 147 | /** 148 | * This callback is displayed as part of the receiveMessage function. 149 | * 150 | * @callback receiveMessageReturn 151 | * @param {number} visibilityTimeout time in seconds when message is visible 152 | * @param {number} waitTimeSeconds time in secods to wait queue for messages 153 | * @returns {Promise} response from aws 154 | */ 155 | -------------------------------------------------------------------------------- /src/ports/state-machines/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Enum for classError values. 3 | * @readonly 4 | * @memberof ports/state-machines 5 | * @enum {string} 6 | */ 7 | export const classError = { 8 | INTERNAL: 'INTERNAL' 9 | } 10 | -------------------------------------------------------------------------------- /src/ports/state-machines/constants.spec.js: -------------------------------------------------------------------------------- 1 | import { classError } from './constants' 2 | 3 | describe('constants', () => { 4 | test('classError', () => { 5 | expect(classError.INTERNAL).toBe('INTERNAL') 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /src/ports/state-machines/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * StateMachines Namespace. 3 | * @namespace ports/state-machines 4 | * 5 | * 6 | * @description this namespace control state-machine api methods 7 | */ 8 | 9 | /** 10 | * reference doc only imports 11 | */ 12 | // eslint-disable-next-line no-unused-vars 13 | import { DynamoDB } from 'aws-sdk' 14 | // eslint-disable-next-line no-unused-vars 15 | import { Moment } from 'moment' 16 | /** 17 | * library references 18 | */ 19 | import { 20 | sendMessage, deleteMessage, receiveMessage, 21 | // eslint-disable-next-line no-unused-vars 22 | sendMessageReturn, deleteMessageReturn, receiveMessageReturn 23 | } from './aws.sqs' 24 | import { updateDocument, getDocument, putDocument, deleteDocument } from './aws.dynamo' 25 | 26 | /*** 27 | * repositories 28 | * dynamo and sqs are used in repositories 29 | */ 30 | 31 | /** 32 | * @description dynamo repository for state machine 33 | * 34 | * @memberof ports/state-machines 35 | * @function 36 | * @param {DynamoDB.DocumentClient} dynamo instance of dynamo api 37 | * @returns {DynamoRepositoryInstance} instance of repository for database 38 | */ 39 | export const databaseRepository = (dynamo, tableName) => { 40 | return { 41 | updateDocument: updateDocument(dynamo, tableName), 42 | getDocument: getDocument(dynamo, tableName), 43 | putDocument: putDocument(dynamo, tableName), 44 | deleteDocument: deleteDocument(dynamo, tableName) 45 | } 46 | } 47 | 48 | /** 49 | * @description queue repository for state machine 50 | * 51 | * @memberof ports/state-machines 52 | * @function 53 | * @param {sqs} sqs instance of SQS 54 | * @param {string} queueUrl queue url string 55 | * @param {number} maxNumberOfMessages max messages to collect in single call 56 | * @returns {QueueRepositoryInstance} 57 | */ 58 | export const queueRepository = (sqs, queueUrl, maxNumberOfMessages) => { 59 | return { 60 | sendMessage: sendMessage(sqs, queueUrl), 61 | receiveMessage: receiveMessage(sqs, queueUrl, maxNumberOfMessages), 62 | deleteMessage: deleteMessage(sqs, queueUrl) 63 | } 64 | } 65 | 66 | /*** 67 | * type definitions for complex objects 68 | * this helps documentation 69 | */ 70 | 71 | /** 72 | * @typedef {Object} PayloadValue 73 | * @property {string} key unique key on tupple 74 | * @property {EVariableType} type type of the key for cast operations 75 | * @property {string} value value of the variable in payload 76 | */ 77 | 78 | /** 79 | * @typedef {Object} QueueRepositoryInstance 80 | * @property {sendMessageReturn} sendMessage function to send message to sqs (instantiated). 81 | * @property {receiveMessageReturn} receiveMessage function to receive message from sqs (instantiated). 82 | * @property {deleteMessageReturn} deleteMessage function to delete message from sqs (instantiated). 83 | */ 84 | 85 | /** 86 | * @typedef {Object} DynamoRepositoryInstance 87 | * 88 | * @property {updateDocumentReturn} updateDocument function to update existing document (instantiated). 89 | * @property {getDocumentReturn} getDocument function to get existing document (instantiated). 90 | * @property {putDocumentReturn} putDocument function to create existing document (instantiated). 91 | * @property {deleteDocumentReturn} deleteDocument function to delete existing document (instantiated). 92 | */ 93 | 94 | /** 95 | * This callback is displayed as part of the updateDocument (inner DynamoRepositoryInstance) function. 96 | * 97 | * @callback updateDocumentReturn 98 | * @param {Object} key - object of keys table parameters to search 99 | * @returns {Object} - object sended 100 | */ 101 | 102 | /** 103 | * This callback is displayed as part of the getDocument (inner DynamoRepositoryInstance) function. 104 | * 105 | * @callback getDocumentReturn 106 | * @param {Object} key - object of keys table parameters to search 107 | * @param {DynamoDB.UpdateExpression} updateExpression dynamo notation of the update document expression without values to change 108 | * @param {Object} expressionAttributeValues values to be mapped in updateExpression expression 109 | * @returns {Object} - object updated from state-machine 110 | */ 111 | 112 | /** 113 | * This callback is displayed as part of the putDocument (inner DynamoRepositoryInstance) function. 114 | * 115 | * @callback putDocumentReturn 116 | * @param {Object} item - object to persist 117 | */ 118 | 119 | /** 120 | * This callback is displayed as part of the deleteDocument (inner DynamoRepositoryInstance) function. 121 | * 122 | * @callback deleteDocumentReturn 123 | * @param {Object} key - key of the data 124 | */ 125 | -------------------------------------------------------------------------------- /src/utils/errors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * common treatments for errors handler 3 | * 4 | * this only one mutable object in all code 5 | */ 6 | 7 | /** 8 | * Object type for Custom errors 9 | * @typedef {Object} CustomError Object 10 | * @property {string} name - class of error from outside. 11 | * @property {string} internalName - class of error from inside (like BadRequest, etc.). 12 | * @property {string} methodPath - method origin of the error 13 | * @property {string} message - detail of error text 14 | * @property {string} stack - Stack call trace from base code call 15 | */ 16 | export class CustomError extends Error { 17 | /** 18 | * @constructs CustomError 19 | * @param {Error} err inherited error class 20 | * @param {string} methodPath method origin of the error 21 | * @param {string} classError class of error from our code 22 | */ 23 | constructor(err, methodPath, classError) { 24 | super() 25 | const { name, message, stack } = err 26 | 27 | // eslint-disable-next-line 28 | this.name = name 29 | // eslint-disable-next-line 30 | this.message = message 31 | // eslint-disable-next-line 32 | this.stack = stack 33 | // eslint-disable-next-line 34 | this.internalName = classError 35 | // eslint-disable-next-line 36 | this.method = methodPath 37 | } 38 | } 39 | 40 | /** 41 | * Enum for EClassError values. 42 | * @readonly 43 | * @memberof utils 44 | * @enum {string} 45 | */ 46 | export const EClassError = { 47 | INTERNAL: 'INTERNAL', 48 | USER_ERROR: 'USER_ERROR' 49 | } 50 | 51 | /** 52 | * Throw correct messagem error from origin in all levels 53 | * of the structure business -> adapter -> ports with correct CustomError 54 | * if the error is typeof CustomError 55 | * @memberof utils 56 | * @function 57 | * @param {Error} err inherited error class 58 | * @param {string} methodPath method origin of the error 59 | * @param {string} classError class of error from our code 60 | * @return {undefined} 61 | */ 62 | export const throwCustomError = (error, methodPath, classError) => { 63 | switch (error.constructor) { 64 | case CustomError: 65 | throw error 66 | default: 67 | throw new CustomError(error, methodPath, classError) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/utils/errors.spec.js: -------------------------------------------------------------------------------- 1 | import { throwCustomError, CustomError, EClassError } from './errors' 2 | 3 | /** 4 | * begin of the test suite 5 | */ 6 | describe('CustomError', () => { 7 | const methodPath = 'custom.path' 8 | 9 | test('basic instantiate', () => { 10 | const throwMessage = 'sample' 11 | const err = new CustomError(new Error(throwMessage), methodPath, EClassError.INTERNAL) 12 | expect(err.internalName).toBe(EClassError.INTERNAL) 13 | expect(err.message).toBe(throwMessage) 14 | expect(err.method).toBe(methodPath) 15 | expect(err.name).toBe('Error') 16 | }) 17 | }) 18 | 19 | describe('EClassError', () => { 20 | test('constants', () => { 21 | expect(EClassError.INTERNAL).toBe('INTERNAL') 22 | expect(EClassError.USER_ERROR).toBe('USER_ERROR') 23 | }) 24 | }) 25 | 26 | describe('throwCustomError', () => { 27 | const methodPath = 'custom.path' 28 | const toSpy = { 29 | throwCustomError 30 | } 31 | jest.spyOn(toSpy, 'throwCustomError') 32 | test('basic call', () => { 33 | const throwMessage = 'sample' 34 | const t = () => { 35 | toSpy.throwCustomError(new Error(throwMessage), methodPath, EClassError.INTERNAL) 36 | } 37 | expect(t).toThrow() 38 | expect(toSpy.throwCustomError).toHaveBeenCalledWith(new Error(throwMessage), methodPath, EClassError.INTERNAL) 39 | }) 40 | 41 | test('call from CustomError', () => { 42 | const throwMessage = 'sample' 43 | const err = new CustomError(new Error(throwMessage), methodPath, EClassError.INTERNAL) 44 | const t = () => { 45 | throwCustomError(err, 'doNotOverride', EClassError.USER_ERROR) 46 | } 47 | expect(t).toThrow() 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Utils Namespace. 3 | * @namespace utils 4 | * 5 | * 6 | * @description this namespace have common methods to use in all scope from repo 7 | */ 8 | 9 | import { CustomError, EClassError, throwCustomError } from './errors' 10 | 11 | export { CustomError, EClassError, throwCustomError } 12 | -------------------------------------------------------------------------------- /stryker.conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('@stryker-mutator/api/core').StrykerOptions} 3 | */ 4 | 5 | module.exports = { 6 | mutator: 'javascript', 7 | packageManager: 'npm', 8 | reporters: ['html', 'clear-text', 'progress'], 9 | testRunner: 'jest', 10 | coverageAnalysis: 'off', 11 | maxConcurrentTestRunners: 2, 12 | dashboard: { 13 | project: 'github.com/claytonsilva/nodejs-hexagonal-boilerplate' 14 | }, 15 | mutate: [ 16 | 'src/**/*.js', 17 | '!src/ports/http/**/*.js', 18 | '!src/ports/aws-lambda/**/*.js', 19 | '!src/ports/logger/**/*.js', 20 | '!src/**/*.spec.js' 21 | ], 22 | jest: { 23 | projectType: 'custom', 24 | configFile: 'jest.config.js', 25 | enableFindRelatedTests: true 26 | }, 27 | timeoutMS: 15000 28 | } 29 | --------------------------------------------------------------------------------