├── .checkbuild ├── .editorconfig ├── .env.dist ├── .eslintrc ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .jsbeautifyrc ├── Dockerfile ├── README.md ├── bootstrap.js ├── bootstrap.test.js ├── circle.yml ├── docker-compose.override.yml ├── docker-compose.yml ├── npm-shrinkwrap.json ├── package.json ├── server.js └── src ├── api ├── api.js ├── definition.js ├── definitions │ ├── Article.js │ ├── Articles.js │ ├── Error.js │ ├── Errors.js │ ├── NewArticle.js │ └── index.js ├── handlers │ ├── articles.js │ ├── articles.test.fixtures.json │ ├── articles.test.js │ └── index.js └── index.js ├── config.test.js ├── config ├── config.js ├── config.json └── index.js ├── domain ├── ArticleModel.js ├── ArticleRepository.fixtures.json ├── ArticleRepository.js └── index.js ├── index.js ├── logger.js └── logger.test.js /.checkbuild: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [".checkbuildrc_base"], 3 | "urls": ["https://raw.githubusercontent.com/iadvize/javascript-convention/master/.checkbuildrc_base"] 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | # 2 space indentation 12 | [*.js] 13 | indent_style = space 14 | indent_size = 2 15 | 16 | # 2 space indentation 17 | [*.ts] 18 | indent_style = space 19 | indent_size = 2 20 | -------------------------------------------------------------------------------- /.env.dist: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ".eslintrc_base", 3 | "globals": { 4 | "_": true, 5 | "when": true, 6 | "assert": true, 7 | "async": true 8 | }, 9 | "rules": { 10 | "no-var": 0 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | 5 | Well, write good code :trollface:. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### Current Behaviour 2 | 3 | #### Expected Behaviour 4 | 5 | #### Steps to reproduce 6 | 7 | - 8 | - 9 | - 10 | 11 | #### Stacktrace 12 | 13 | ` 14 | 15 | ` 16 | 17 | #### Logs 18 | 19 | ` 20 | 21 | ` 22 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### What's this PR purpose? 2 | > Ticket link [IDZ-](https://iadvize.atlassian.net/browse/IDZ) 3 | 4 | ##### DESCRIPTION 5 | - 6 | - 7 | - 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | # don't commit .env as it should be local to your dev environment 30 | .env 31 | 32 | # don't commit .checkbuildrc_base as it will be downloaded each time check-build run 33 | .checkbuildrc_base 34 | 35 | # don't commit .jscsrc as it will be downloaded each time check-build run 36 | .jscsrc 37 | 38 | # same for jshintrc_base 39 | .jshintrc_base 40 | 41 | # same for .eslintrc_base 42 | .eslintrc_base 43 | 44 | # don't commit junit.xml 45 | junit.xml 46 | 47 | .idea 48 | -------------------------------------------------------------------------------- /.jsbeautifyrc: -------------------------------------------------------------------------------- 1 | { 2 | "indent_with_tabs": false, 3 | "max_preserve_newlines": 4, 4 | "preserve_newlines": true, 5 | "space_in_paren": false, 6 | "jslint_happy": true, 7 | "brace_style": "collapse", 8 | "keep_array_indentation": true, 9 | "keep_function_indentation": true, 10 | "eval_code": false, 11 | "unescape_strings": false, 12 | "break_chained_methods": false, 13 | "e4x": false, 14 | "wrap_line_length": 0, 15 | "format_on_save":true 16 | } 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ## 2 | # NAME : iadvize/my-first-nodejs-service 3 | # VERSION : latest 4 | # DOCKER-VERSION : 1.5 5 | # DESCRIPTION : 6 | # TO_BUILD : docker build --pull=true --no-cache -t iadvize/my-first-nodejs-service . 7 | # TO_SHIP : docker push iadvize/my-first-nodejs-service 8 | # TO_RUN : docker run -d iadvize/my-first-nodejs-service 9 | ## 10 | 11 | FROM iadvize/nodejs:4 12 | 13 | COPY .npmrc /root/.npmrc 14 | 15 | COPY package.json /app/package.json 16 | 17 | WORKDIR /app 18 | 19 | RUN npm install 20 | 21 | COPY . /app 22 | 23 | EXPOSE 8080 24 | 25 | CMD ["node", "server.js"] 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | NodeJS Service Boilerplate (iAdvize workshop) 2 | ============================================= 3 | 4 | ## Watch the keynote 5 | 6 |
9 | 10 | ## Watch the screencast and slides 11 | 12 | [](https://www.youtube.com/watch?v=5fYogApDqHY) 13 | 14 | 15 | ## Setup 16 | 17 | [Nvm should be installed](https://github.com/creationix/nvm#install-script) 18 | 19 | ```bash 20 | nvm install 21 | nvm use 22 | npm install 23 | ``` 24 | 25 | ## [Service topology](https://services-mapping.clever.iadvize.com/) 26 | 27 | Format: 28 | 29 | > - protocol=HTTP repository=github-repository-name 30 | 31 | ## Start 32 | 33 | ```bash 34 | npm run start 35 | ``` 36 | 37 | ## API documentation 38 | 39 | ```bash 40 | # start the server and then : 41 | open http://127.0.0.1:8080/api-docs 42 | ``` 43 | 44 | ## Tests 45 | 46 | #### Run tests locally 47 | 48 | ```bash 49 | npm test 50 | ``` 51 | 52 | #### Automatically reload tests while coding 53 | 54 | ``` 55 | npm run test-watch 56 | ``` 57 | 58 | #### Test coverage 59 | 60 | ``` 61 | open test/coverage/lcov-report/index.html 62 | ``` 63 | 64 | A cobertura xml file is also available in `test/coverage/cobertura-coverage.xml`. 65 | 66 | 67 | ## Continuous Integration 68 | 69 | #### Setup Jenkins 70 | 71 | ```bash 72 | #!/bin/bash 73 | cd $WORKSPACE 74 | export VAR_1=PRIVATE_VALUE_1 75 | export VAR_2=PRIVATE_VALUE_2 76 | # and so on... 77 | source ./scripts/ci/start 78 | ``` 79 | 80 | #### Debug locally 81 | 82 | ```bash 83 | npm run ci 84 | ``` 85 | 86 | ## Versioning 87 | 88 | This project follows the [Semantic Versioning 2.0.0](http://semver.org/). 89 | -------------------------------------------------------------------------------- /bootstrap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // because JavaScript has a limited standard lib, 4 | // we want to globally expose some libraries we use in every files 5 | // instead of require them everytime. #tradeoff 6 | global.assert = require('assert'); 7 | global.when = require('when'); 8 | global._ = require('lodash'); 9 | -------------------------------------------------------------------------------- /bootstrap.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // You must require this file inside each tests 4 | // or use mocha -r bootstrap.test.js 5 | // (-r === require file) 6 | 7 | require('./bootstrap'); 8 | 9 | global.t = require('chai').assert; 10 | 11 | global.JSONPackage = require('./package.json'); 12 | 13 | global.getServer = () => { 14 | return require('./src')(global.JSONPackage, { 15 | info: _.noop, 16 | debug: _.noop, 17 | log: _.noop, 18 | error: console.error.bind(console) // eslint-disable-line no-console 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | override: 3 | - nvm exec $(node -e 'console.log(require("./package.json").engines.node);') npm install 4 | cache_directories: 5 | - "~/.npm" 6 | test: 7 | override: 8 | - nvm exec $(node -e 'console.log(require("./package.json").engines.node);') npm test 9 | post: 10 | - $(npm bin)/check-build 11 | 12 | # deployment: 13 | # clever: 14 | # branch: master 15 | # commands: 16 | # - ./scripts/ci/deploy_dev 17 | # - ./scripts/ci/deploy_preprod 18 | # - ./scripts/ci/deploy_prod 19 | -------------------------------------------------------------------------------- /docker-compose.override.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | my-first-nodejs: 4 | build: . 5 | command: npm run start 6 | volumes: 7 | - .:/app 8 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | my-first-nodejs: 4 | image: iadvize/my-first-nodejs:master 5 | ports: 6 | - "8080:8080" 7 | env_file: .env 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iadvize-my-first-nodejs-service", 3 | "title": "my-first-nodejs-service", 4 | "private": true, 5 | "version": "3.0.0", 6 | "description": "my first node js service", 7 | "main": "server.js", 8 | "scripts": { 9 | "ci": "npm test", 10 | "test": "nyc --report-dir=${CIRCLE_ARTIFACTS:-'./test/coverage'} --all --statements=86 --lines=86 --functions=100 --branches=61 --check-coverage --reporter=lcov --reporter=cobertura -- mocha -r bootstrap.test.js $(find src -name '*.test.js')", 11 | "test-watch": "mocha -w -G -r bootstrap.test.js $(find src -name '*.test.js')", 12 | "check-build": "check-build", 13 | "postinstall": "npm outdated", 14 | "start": "node server.js" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/iadvize/my-first-nodejs-service.git" 19 | }, 20 | "author": { 21 | "name": "Francois-Guillaume Ribreau", 22 | "url": "http://fgribreau.com/", 23 | "email": "npm@fgribreau.com" 24 | }, 25 | "engines": { 26 | "node": "5" 27 | }, 28 | "license": { 29 | "name": "iAdvize", 30 | "url": "http://iadvize.com" 31 | }, 32 | "bugs": { 33 | "url": "https://github.com/iadvize/my-first-nodejs-service/issues" 34 | }, 35 | "homepage": "https://github.com/iadvize/my-first-nodejs-service", 36 | "dependencies": { 37 | "async": "1.x.x", 38 | "boom": "^3.0.0", 39 | "common-env": "5.x.x", 40 | "deepmerge": "0.2.x", 41 | "hapi": "13.x.x", 42 | "lodash": "4.x.x", 43 | "node-uuid": "^1.4.7", 44 | "redsmin-api-documentation-ui": "^3.0.1", 45 | "swaggerize-hapi": "1.x.x", 46 | "wascally": "0.x.x", 47 | "when": "3.x.x", 48 | "winston": "2.x.x", 49 | "winston-logmatic": "^0.2.0" 50 | }, 51 | "devDependencies": { 52 | "chai": "3.x.x", 53 | "check-build": "*", 54 | "mocha": "2.x.x", 55 | "nyc": "^6.4.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./bootstrap'); 4 | 5 | const JSONPackage = require('./package.json'); 6 | 7 | // we need to look inside src/ even if it breaks separation of concerns (because we deal here with the chicken & egg issue) 8 | const logger = require('./src/logger')(JSONPackage); 9 | 10 | // change default node process name, useful for `ps` 11 | process.title = JSONPackage.title; 12 | 13 | require('./src')(JSONPackage, logger).done((pair) => { 14 | logger.info('Server started at %s', pair.config.api.port); 15 | }, (err) => { 16 | logger.error('Server start error', String(err)); 17 | throw err; 18 | }); 19 | -------------------------------------------------------------------------------- /src/api/api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Hapi = require('hapi'); 4 | const Swaggerize = require('swaggerize-hapi'); 5 | const activateSwagger = require('redsmin-api-documentation-ui/hapi'); 6 | 7 | module.exports = function(packageJson, PORT, models) { 8 | 9 | const server = new Hapi.Server(); 10 | 11 | server.connection({ 12 | port: PORT 13 | }); 14 | 15 | return when(server.register([{ 16 | register: Swaggerize, 17 | options: { 18 | api: require('./definition')(packageJson), 19 | docspath: '/api/definition.json', 20 | handlers: require('./handlers')(models) 21 | } 22 | }])) 23 | .then(activateSwagger(server)) 24 | .then(function() { 25 | return when.promise(function(resolve, reject) { 26 | server.start(function(err) { 27 | if (err) { 28 | return reject(err); 29 | } 30 | 31 | resolve(server); 32 | }); 33 | }); 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /src/api/definition.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var deepExtend = require('deepmerge'); 3 | 4 | module.exports = function(packageJson) { 5 | /** 6 | * Define a new route 7 | * @param {Object} options 8 | * @return {Object} extended `options` with default parameters 9 | */ 10 | function def(options) { 11 | return deepExtend({ 12 | tags: ['my-first-nodejs-service'], 13 | summary: 'not-defined', 14 | description: 'not-defined', 15 | operationId: 'not-defined', 16 | produces: ['application/vnd.api+json'], 17 | parameters: [], 18 | responses: { 19 | 200: { 20 | description: 'OK', 21 | schema: { 22 | type: 'object', 23 | required: ['data'] 24 | } 25 | }, 26 | 400: { 27 | description: 'Bad Request', 28 | schema: { 29 | $ref: '#/definitions/Errors' 30 | } 31 | }, 32 | 404: { 33 | description: 'Not Found', 34 | schema: { 35 | $ref: '#/definitions/Errors' 36 | } 37 | }, 38 | 500: { 39 | description: 'Internal Server Error', 40 | schema: { 41 | $ref: '#/definitions/Errors' 42 | } 43 | } 44 | } 45 | }, options); 46 | } 47 | 48 | return { 49 | swagger: '2.0', 50 | schemes: [ 51 | 'http' 52 | ], 53 | info: { 54 | title: packageJson.title, 55 | description: packageJson.description, 56 | contact: packageJson.author, 57 | license: packageJson.license, 58 | version: packageJson.version 59 | }, 60 | consumes: [ 61 | 'application/vnd.api+json' 62 | ], 63 | produces: [ 64 | 'application/vnd.api+json' 65 | ], 66 | 67 | /** 68 | * Definitions 69 | */ 70 | definitions: require('./definitions'), 71 | 72 | /** 73 | * Routes 74 | */ 75 | paths: { 76 | '/articles': { 77 | get: def({ 78 | summary: 'List articles', 79 | description: 'List all the articles', 80 | operationId: 'listArticles', 81 | responses: { 82 | 200: { 83 | description: 'List of articles', 84 | schema: { 85 | type: 'object', 86 | required: 'data', 87 | properties: { 88 | data: { 89 | $ref: '#/definitions/Articles' 90 | } 91 | } 92 | } 93 | } 94 | } 95 | }), 96 | post: def({ 97 | summary: 'Post article', 98 | description: 'Post a new article', 99 | operationId: 'postArticle', 100 | parameters: [{ 101 | 'in': 'body', 102 | name: 'newArticle', 103 | description: 'new Article object to post', 104 | required: true, 105 | schema: { 106 | $ref: '#/definitions/NewArticle' 107 | } 108 | }], 109 | responses: { 110 | 200: { 111 | description: 'New article response', 112 | schema: { 113 | type: 'object', 114 | required: 'data', 115 | properties: { 116 | data: { 117 | $ref: '#/definitions/Article' 118 | } 119 | } 120 | } 121 | } 122 | } 123 | }) 124 | }, 125 | '/articles/{articleId}': { 126 | get: def({ 127 | summary: 'Get article', 128 | description: 'Get a single article by id', 129 | operationId: 'getArticleById', 130 | parameters: [{ 131 | 'in': 'path', 132 | name: 'articleId', 133 | description: 'Identifier of the requested article', 134 | type: 'string', 135 | format: 'uuid', // @todo currently uuid are not checked 136 | required: true 137 | }], 138 | responses: { 139 | 200: { 140 | description: 'Article response', 141 | schema: { 142 | type: 'object', 143 | required: 'data', 144 | properties: { 145 | data: { 146 | $ref: '#/definitions/Article' 147 | } 148 | } 149 | } 150 | } 151 | } 152 | }) 153 | } 154 | } 155 | }; 156 | }; 157 | -------------------------------------------------------------------------------- /src/api/definitions/Article.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | type: 'object', 5 | required: [ 6 | 'id', 7 | 'type', 8 | 'attributes' 9 | ], 10 | properties: { 11 | id: { 12 | type: 'string' 13 | }, 14 | type: { 15 | type: 'string' 16 | }, 17 | attributes: { 18 | type: 'object', 19 | properties: { 20 | title: { 21 | type: 'string' 22 | } 23 | } 24 | } 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/api/definitions/Articles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | type: 'array', 5 | items: { 6 | $ref: '#/definitions/Article' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /src/api/definitions/Error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | type: 'object', 5 | required: [ 6 | 'id', 7 | 'status', 8 | 'code', 9 | 'title' 10 | ], 11 | properties: { 12 | id: { 13 | type: 'string' 14 | }, 15 | status: { 16 | type: 'string' 17 | }, 18 | code: { 19 | type: 'string' 20 | }, 21 | title: { 22 | type: 'string' 23 | }, 24 | detail: { 25 | type: 'string' 26 | } 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/api/definitions/Errors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | type: 'object', 5 | required: ['errors'], 6 | properties: { 7 | errors: { 8 | type: 'array', 9 | items: { 10 | $ref: '#/definitions/Error' 11 | } 12 | } 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/api/definitions/NewArticle.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | type: 'object', 5 | required: [ 6 | 'title' 7 | ], 8 | properties: { 9 | title: { 10 | type: 'string', 11 | minLength: 1 12 | } 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/api/definitions/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var definitions = { 4 | Articles: require('./Articles'), 5 | Article: require('./Article'), 6 | NewArticle: require('./NewArticle'), 7 | Errors: require('./Errors'), 8 | Error: require('./Error') 9 | }; 10 | 11 | module.exports = definitions; 12 | -------------------------------------------------------------------------------- /src/api/handlers/articles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Boom = require('boom'); 4 | 5 | const HTTP_OK = 200; 6 | const HTTP_CREATED = 201; 7 | 8 | module.exports = function(ArticleRepository, ArticleModel) { 9 | assert(_.isPlainObject(ArticleRepository)); 10 | assert(_.isFunction(ArticleModel)); 11 | 12 | return { 13 | '{articleId}': { 14 | $get: function(request, reply) { 15 | ArticleRepository.findById(request.params.articleId, (err, article) => { 16 | if (err) { 17 | return reply(Boom.badImplementation(err)); 18 | } 19 | 20 | if (!article) { 21 | return reply(Boom.notFound()); 22 | } 23 | 24 | reply(article).code(HTTP_OK); 25 | }); 26 | } 27 | }, 28 | 29 | $get: function(request, reply) { 30 | ArticleRepository.getAll((err, articles) => { 31 | if (err) { 32 | return reply(Boom.badImplementation(err)); 33 | } 34 | 35 | reply(articles).code(HTTP_OK); 36 | }); 37 | }, 38 | 39 | $post: function(request, reply) { 40 | ArticleRepository.create(ArticleModel.withTitle(request.payload.title), (err) => { 41 | if (err) { 42 | return reply(Boom.badImplementation(err)); 43 | } 44 | 45 | reply().code(HTTP_CREATED); 46 | }); 47 | } 48 | }; 49 | }; 50 | -------------------------------------------------------------------------------- /src/api/handlers/articles.test.fixtures.json: -------------------------------------------------------------------------------- 1 | { 2 | "articles": [{ 3 | "id": "3a9c6858-ae67-4b80-a056-43234719f515", 4 | "title": "hello" 5 | }, { 6 | "id": "51ccd13d-8409-404c-87c6-2117b3bf2779", 7 | "title": "world" 8 | }], 9 | "article": { 10 | "id": "3a9c6858-ae67-4b80-a056-43234719f515", 11 | "title": "hello" 12 | }, 13 | "errors": { 14 | "missingTitle": { 15 | "statusCode": 400, 16 | "error": "Bad Request", 17 | "message": "child \"title\" fails because [\"title\" is not allowed to be empty]", 18 | "validation": { 19 | "source": "payload", 20 | "keys": ["title"] 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/api/handlers/articles.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('/articles', () => { 4 | let server; 5 | const fixtures = require('./articles.test.fixtures.json'); 6 | 7 | beforeEach((f) => { 8 | getServer().done(pair => { 9 | assert(_.isObject(pair.server)); 10 | server = pair.server; 11 | f(); 12 | }, f); 13 | }); 14 | 15 | afterEach(f => { 16 | server.stop({ 17 | timeout: 60000 18 | }, function() { 19 | f(); 20 | }); 21 | }) 22 | 23 | describe('GET', () => { 24 | it('should yield every articles', (f) => { 25 | when(server.inject({ 26 | method: 'GET', 27 | url: '/articles' 28 | })).done((res) => { 29 | t.deepEqual(res.result, fixtures.articles); 30 | f(); 31 | }, f); 32 | }); 33 | }); 34 | 35 | describe('POST', () => { 36 | it('should yield an error if title was not specified in payload', (f) => { 37 | when(server.inject({ 38 | method: 'POST', 39 | url: '/articles', 40 | payload: { 41 | title: '' 42 | } 43 | })).done(res => { 44 | t.strictEqual(res.statusCode, 400); 45 | t.deepEqual(res.result, fixtures.errors.missingTitle); 46 | f(); 47 | }); 48 | }); 49 | 50 | it('should insert a new article', (f) => { 51 | when(server.inject({ 52 | method: 'POST', 53 | url: '/articles', 54 | payload: { 55 | title: 'hello world!' 56 | } 57 | })).then(res => { 58 | t.strictEqual(res.statusCode, 201); 59 | }).then(() => server.inject({ 60 | method: 'GET', 61 | url: '/articles' 62 | })).done(res => { 63 | t.strictEqual(res.result.length, 3); 64 | f(); 65 | }, f); 66 | }); 67 | }); 68 | 69 | describe('/{articleId}', () => { 70 | describe('GET', () => { 71 | it('should yield an error if no `articleId` was specified or invalid', (f) => { 72 | when(server.inject({ 73 | method: 'GET', 74 | url: '/articles/12' 75 | })).done(res => { 76 | t.strictEqual(res.statusCode, 404); 77 | t.deepEqual(res.result, {"statusCode":404,"error":"Not Found"}); 78 | f(); 79 | }); 80 | }); 81 | 82 | it('should yield an article', (f) => { 83 | when(server.inject({ 84 | method: 'GET', 85 | url: '/articles/3a9c6858-ae67-4b80-a056-43234719f515' 86 | })).done(res => { 87 | t.strictEqual(res.statusCode, 200); 88 | t.deepEqual(res.result, fixtures.article); 89 | f(); 90 | }); 91 | }); 92 | }); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /src/api/handlers/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (domain) => { 4 | assert(_.isPlainObject(domain)); 5 | 6 | return { 7 | articles: require('./articles')(domain.ArticleRepository, domain.ArticleModel) 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./api'); 4 | -------------------------------------------------------------------------------- /src/config.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('config', () => { 4 | var config; 5 | var silentLogger = { 6 | info: _.noop 7 | }; 8 | 9 | beforeEach(() => { 10 | delete process.env.API_PORT; 11 | loadConfig(); 12 | }); 13 | 14 | it('should yield the default config.port number', () => { 15 | t.strictEqual(config.api.port, 8080); 16 | }); 17 | 18 | describe('with env variables', () => { 19 | beforeEach(() => { 20 | process.env.API_PORT = 8081; 21 | loadConfig(); 22 | }); 23 | 24 | it('should yield the config.port number from the env variable if defined', () => { 25 | t.strictEqual(config.api.port, 8081); 26 | }); 27 | }); 28 | 29 | // helpers 30 | function loadConfig() { 31 | config = require('./config')(silentLogger); 32 | } 33 | }); 34 | -------------------------------------------------------------------------------- /src/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(logger) { 4 | var env = require('common-env/withLogger')(logger); 5 | var configAsJson = require('./config.json'); 6 | return env.getOrElseAll(configAsJson); 7 | }; 8 | -------------------------------------------------------------------------------- /src/config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "api": { 3 | "port": 8080 4 | }, 5 | "logger": { 6 | "timestamp": true, 7 | "level": "debug", 8 | "transports": { 9 | "console": { 10 | "enabled": true, 11 | "config": { 12 | "colorize": true, 13 | "timestamp": true 14 | } 15 | }, 16 | "logmatic": { 17 | "enabled": false, 18 | "config": { 19 | "token": "logmatic-ftw-yeah-boyz-(and-girlz-of-course)", 20 | "hostname": "localhost" 21 | } 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./config'); 4 | -------------------------------------------------------------------------------- /src/domain/ArticleModel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const uuid = require('node-uuid').v4; 3 | 4 | module.exports = () => { 5 | 6 | /** 7 | * [ArticleModel description] 8 | * @param {UUID} id [description] 9 | * @param {[type]} title [description] 10 | */ 11 | function ArticleModel(id, title) { 12 | this.id = id; 13 | this.title = title; 14 | } 15 | 16 | ArticleModel.withTitle = function(title) { 17 | return new ArticleModel(uuid(), title); 18 | }; 19 | 20 | return ArticleModel; 21 | }; 22 | -------------------------------------------------------------------------------- /src/domain/ArticleRepository.fixtures.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "id": "3a9c6858-ae67-4b80-a056-43234719f515", 3 | "title": "hello" 4 | }, { 5 | "id": "51ccd13d-8409-404c-87c6-2117b3bf2779", 6 | "title": "world" 7 | }] 8 | -------------------------------------------------------------------------------- /src/domain/ArticleRepository.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (ArticleModel) => { 4 | assert(_.isFunction(ArticleModel)); 5 | 6 | // only for testing 7 | const db = _.cloneDeep(require('./ArticleRepository.fixtures.json')); 8 | 9 | return { 10 | /** 11 | * Get every articles 12 | * @param {function} f(err, articles) yield an error if an error ocurred, otherwise an array (empty or not) containing ArticleModels 13 | */ 14 | getAll: (f) => { 15 | f(null, db.map((json) => new ArticleModel(json.id, json.title))); 16 | }, 17 | 18 | findById: (uuid, f) => { 19 | f(null, _.find(db, { 20 | id: uuid 21 | })); 22 | }, 23 | 24 | create: (articleModel, f) => { 25 | if (!(articleModel instanceof ArticleModel)) { 26 | return f(new Error('Invalid articleModel')); 27 | } 28 | 29 | // simulate a serialize 30 | db.push(JSON.parse(JSON.stringify(articleModel))); 31 | 32 | f(null); 33 | } 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /src/domain/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = () => { 4 | const ArticleModel = require('./ArticleModel')(); 5 | 6 | return { 7 | ArticleModel: ArticleModel, 8 | ArticleRepository: require('./ArticleRepository')(ArticleModel) 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(packageJson, logger) { 4 | const config = require('./config')(logger); 5 | const createServer = require('./api'); 6 | const domain = require('./domain')(); 7 | 8 | return createServer(packageJson, config.api.port, domain).then((server) => { 9 | return {server, config, domain}; 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /src/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Winston = require('winston'); 4 | const WinstonLogmatic = require('winston-logmatic'); 5 | 6 | module.exports = (packageJson) => { 7 | assert(_.isString(packageJson.name) && packageJson); 8 | 9 | /** 10 | * Here we have the issue of the chicken and the egg (we want to log inside logmatic common-env startup) 11 | * So instead of using process.env, we leverage common-env with winston default logger to only retrieve logmatic related configuration 12 | */ 13 | const config = require('common-env/withLogger')(Winston).getOrElseAll({ 14 | logger: require('./config/config.json').logger 15 | }).logger; 16 | 17 | let transports = []; 18 | 19 | if (config.transports.console.enabled) { 20 | transports.push(new Winston.transports.Console(config.transports.console.config)); 21 | } 22 | 23 | if (config.transports.logmatic.enabled) { 24 | transports.push(new WinstonLogmatic({ 25 | logmatic: { 26 | token: config.transports.logmatic.config.token, 27 | defaultProps: { 28 | appname: packageJson.name, 29 | hostname: config.transports.logmatic.config.hostname 30 | } 31 | } 32 | })); 33 | } 34 | 35 | return new Winston.Logger({ 36 | transports: transports, 37 | timestamp: config.timestamp, 38 | level: config.level 39 | }); 40 | }; 41 | -------------------------------------------------------------------------------- /src/logger.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('logger', () => { 4 | const logger = require('./logger')(JSONPackage); 5 | 6 | it('should allow us to log', () => { 7 | t.isFunction(logger.info); 8 | t.isFunction(logger.debug); 9 | }); 10 | }) 11 | --------------------------------------------------------------------------------