├── .env.example ├── .gitignore ├── README.md ├── config.js ├── index.js ├── lib ├── api.js ├── authify.js ├── db.js ├── models │ └── things.js └── server.js ├── package-lock.json ├── package.json └── test ├── index.js ├── models.js └── routes.js /.env.example: -------------------------------------------------------------------------------- 1 | DB_PATH=./db-example 2 | AUTHENTIC_HOST=https://ix-id.lincx.la 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | coverage 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Git Challenge 2 | There are two branches, `add-echo` and `add-reverse`. The goal of this challenge is to use `git rebase` to bring both commits onto master. When finished there should be no merge commits or branching. For example, `git log` on the `master` branch should look similar to this: 3 | ``` 4 | /challenge-git master 5 | ⚡ git log 6 | 61a2c67 feat: add reverse route (David Guttman, 7 minutes ago) 7 | 2c2c5d6 feat: add echo route (David Guttman, 10 minutes ago) 8 | dcc4c0b docs: add more instructions (David Guttman, 11 minutes ago) 9 | ... 10 | ``` 11 | ## Instructions 12 | How to attempt this challenge: 13 | 1) Create a new repo in your account and note the git url 14 | 2) Clone this repo 15 | 3) Solve the challenge 16 | 4) Set your new repo as the origin: `git remote set-url origin ${your repo url}` 17 | 5) Push your solution to your repo 18 | You must follow these steps for your solution to be accepted -- forks or other methods will not be considered. 19 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | var path = require('path') 3 | 4 | module.exports = { 5 | level: { 6 | location: process.env.DB_PATH || path.join(__dirname, './db') 7 | }, 8 | authentic: { 9 | host: process.env.AUTHENTIC_HOST || 'https://ix-id.lincx.la' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var name = require('./package.json').name 2 | require('productionize')(name) 3 | 4 | var server = require('./lib/server') 5 | var port = process.env.PORT || 5000 6 | server().listen(port) 7 | console.log(name, 'listening on port', port) 8 | -------------------------------------------------------------------------------- /lib/api.js: -------------------------------------------------------------------------------- 1 | var ss = require('serialize-stream') 2 | var pump = require('pump') 3 | var body = require('body/json') 4 | var send = require('send-data/json') 5 | 6 | var Things = require('./models/things') 7 | 8 | module.exports = { 9 | get: get, 10 | put: put, 11 | echo: echo, 12 | stream: stream, 13 | reverse: reverse 14 | } 15 | 16 | function get (req, res, opts, cb) { 17 | Things.get(opts.params.key, function (err, value) { 18 | if (err) return cb(err) 19 | 20 | send(req, res, value) 21 | }) 22 | } 23 | 24 | function put (req, res, opts, cb) { 25 | body(req, res, function (err, data) { 26 | if (err) return cb(err) 27 | 28 | Things.put(opts.params.key, data, function (err) { 29 | if (err) return cb(err) 30 | 31 | send(req, res, data) 32 | }) 33 | }) 34 | } 35 | 36 | function echo (req, res, opts, cb) { 37 | send(req, res, opts.query) 38 | } 39 | 40 | function stream (req, res, opts, cb) { 41 | pump( 42 | Things.createValueStream({ 43 | gte: opts.params.gte, 44 | lte: opts.params.lte 45 | }), 46 | ss(opts.query.format), 47 | res, 48 | cb 49 | ) 50 | } 51 | 52 | function reverse (req, res, opts, cb) { 53 | var input = opts.params.input 54 | var output = '' 55 | for (var i = input.length - 1; i >= 0; i--) { 56 | output += input[i] 57 | } 58 | send(req, res, {input, output}) 59 | } 60 | -------------------------------------------------------------------------------- /lib/authify.js: -------------------------------------------------------------------------------- 1 | var xtend = require('xtend') 2 | var Authentic = require('authentic-service') 3 | 4 | var config = require('../config') 5 | 6 | var authentic = process.env.NODE_ENV === 'test' 7 | ? testAuthentic 8 | : Authentic({ server: config.authentic.host }) 9 | 10 | module.exports = function (fn) { 11 | return function authify (req, res, opts, cb) { 12 | authentic(req, res, function (err, creds) { 13 | if (err) return cb(err) 14 | if (!creds || !creds.email) { 15 | return cb(createAuthError('Invalid Credentials')) 16 | } 17 | 18 | req.auth = creds 19 | var authorized = false 20 | if (creds.email.match(/@lincx.la$/)) authorized = true 21 | if (creds.email.match(/@interlincx\.com$/)) authorized = true 22 | 23 | if (authorized) return fn(req, res, xtend(opts, {auth: creds}), cb) 24 | 25 | cb(createAuthError('Unauthorized: ' + creds.email)) 26 | }) 27 | } 28 | } 29 | 30 | function testAuthentic (req, res, cb) { 31 | return cb(null, {email: 'test@interlincx.com'}) 32 | } 33 | 34 | function createAuthError (msg) { 35 | var err = new Error(msg || 'Unauthorized') 36 | err.statusCode = 401 37 | return err 38 | } 39 | -------------------------------------------------------------------------------- /lib/db.js: -------------------------------------------------------------------------------- 1 | var levelup = require('levelup') 2 | var config = require('../config') 3 | 4 | var engine = { 5 | test: require('memdown'), 6 | production: require('mongodown'), 7 | development: require('leveldown') 8 | }[process.env.NODE_ENV] 9 | 10 | var db = module.exports = levelup(config.level.location, { 11 | db: engine, 12 | valueEncoding: 'json' 13 | }) 14 | 15 | db.healthCheck = function (cb) { 16 | var now = Date.now() 17 | db.put('!healthCheck', now, function (err) { 18 | if (err) return cb(err) 19 | db.get('!healthCheck', function (err, then) { 20 | if (err) return cb(err) 21 | if (now !== then) return cb(new Error('DB write failed')) 22 | cb() 23 | }) 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /lib/models/things.js: -------------------------------------------------------------------------------- 1 | var sublevel = require('level-sublevel') 2 | 3 | var db = sublevel(require('../db'), 'things') 4 | 5 | module.exports = db 6 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | var URL = require('url') 2 | var http = require('http') 3 | var cuid = require('cuid') 4 | var Corsify = require('corsify') 5 | var sendJson = require('send-data/json') 6 | var ReqLogger = require('req-logger') 7 | var healthPoint = require('healthpoint') 8 | var HttpHashRouter = require('http-hash-router') 9 | 10 | var db = require('./db') 11 | var api = require('./api') 12 | var authify = require('./authify') 13 | var version = require('../package.json').version 14 | 15 | var router = HttpHashRouter() 16 | var logger = ReqLogger({ version: version }) 17 | var health = healthPoint({ version: version }, db.healthCheck) 18 | var cors = Corsify({ 19 | 'Access-Control-Allow-Origin': '*', 20 | 'Access-Control-Allow-Headers': 'authorization, accept, content-type' 21 | }) 22 | 23 | router.set('/favicon.ico', empty) 24 | router.set('/echo', api.echo) 25 | router.set('/reverse/:input', api.reverse) 26 | router.set('/things/get/:key', { GET: authify(api.get) }) 27 | router.set('/things/put/:key', { POST: authify(api.put) }) 28 | router.set('/things/stream/:gte/:lte', { GET: authify(api.stream) }) 29 | 30 | module.exports = function createServer () { 31 | return http.createServer(cors(handler)) 32 | } 33 | 34 | function handler (req, res) { 35 | if (req.url === '/health') return health(req, res) 36 | req.id = cuid() 37 | logger(req, res, { requestId: req.id }, function (info) { 38 | info.authEmail = (req.auth || {}).email 39 | console.log(info) 40 | }) 41 | router(req, res, { query: getQuery(req.url) }, onError.bind(null, req, res)) 42 | } 43 | 44 | function onError (req, res, err) { 45 | if (!err) return 46 | 47 | res.statusCode = err.statusCode || 500 48 | logError(req, res, err) 49 | 50 | sendJson(req, res, { 51 | error: err.message || http.STATUS_CODES[res.statusCode] 52 | }) 53 | } 54 | 55 | function logError (req, res, err) { 56 | if (process.env.NODE_ENV === 'test') return 57 | 58 | var logType = res.statusCode >= 500 ? 'error' : 'warn' 59 | 60 | console[logType]({ 61 | err: err, 62 | requestId: req.id, 63 | statusCode: res.statusCode 64 | }, err.message) 65 | } 66 | 67 | function empty (req, res) { 68 | res.writeHead(204) 69 | res.end() 70 | } 71 | 72 | function getQuery (url) { 73 | return URL.parse(url, true).query 74 | } 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-server", 3 | "version": "1.0.0", 4 | "description": "This is an example of a basic HTTP API server.", 5 | "main": "index.js", 6 | "directories": { 7 | "lib": "lib", 8 | "test": "test" 9 | }, 10 | "scripts": { 11 | "start": "node index.js", 12 | "dev": "nodemon index.js", 13 | "test": "nyc node test/index.js && npm run deps && standard", 14 | "deps": "npm run deps-missing && npm run deps-extra", 15 | "deps-missing": "dependency-check --no-dev .", 16 | "deps-extra": "dependency-check --no-dev --extra ." 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/Interlincx/example-server.git" 21 | }, 22 | "keywords": [], 23 | "author": "David Guttman (http://davidguttman.com/)", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/Interlincx/example-server/issues" 27 | }, 28 | "homepage": "https://github.com/Interlincx/example-server#readme", 29 | "devDependencies": { 30 | "dependency-check": "^2.10.1", 31 | "nodemon": "^1.19.3", 32 | "nyc": "^11.9.0", 33 | "servertest": "^1.2.1", 34 | "split2": "^2.2.0", 35 | "standard": "^10.0.3", 36 | "tape": "^4.11.0" 37 | }, 38 | "dependencies": { 39 | "authentic-service": "^0.3.1", 40 | "body": "^5.1.0", 41 | "corsify": "^2.1.0", 42 | "cuid": "^1.3.8", 43 | "dotenv": "^4.0.0", 44 | "healthpoint": "^1.0.0", 45 | "http-hash-router": "^1.1.2", 46 | "level-sublevel": "^6.6.5", 47 | "levelup": "^1.3.9", 48 | "memdown": "^1.4.1", 49 | "leveldown": "^5.2.1", 50 | "mongodown": "^1.2.0", 51 | "productionize": "^4.1.0", 52 | "pump": "^1.0.3", 53 | "req-logger": "^2.0.0", 54 | "send-data": "^8.0.0", 55 | "serialize-stream": "^1.1.0", 56 | "xtend": "^4.0.2" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'test' 2 | 3 | require('./routes') 4 | require('./models') 5 | -------------------------------------------------------------------------------- /test/models.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'test' 2 | 3 | var tape = require('tape') 4 | 5 | var Things = require('../lib/models/things') 6 | 7 | tape('should store and retrieve thing', function (t) { 8 | var key = 'thing-A' 9 | var value = {cat: 'hat'} 10 | Things.put(key, value, function (err) { 11 | t.ifError(err, 'should not error') 12 | 13 | Things.get(key, function (err, doc) { 14 | t.ifError(err, 'should not error') 15 | 16 | t.deepEqual(doc.cat, value.cat, 'should match') 17 | t.end() 18 | }) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /test/routes.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'test' 2 | 3 | var tape = require('tape') 4 | var split = require('split2') 5 | var servertest = require('servertest') 6 | 7 | var server = require('../lib/server') 8 | var Things = require('../lib/models/things') 9 | 10 | tape('healthcheck', function (t) { 11 | var url = '/health' 12 | servertest(server(), url, {encoding: 'json'}, function (err, res) { 13 | t.ifError(err, 'no error') 14 | 15 | t.equal(res.statusCode, 200, 'correct statusCode') 16 | t.equal(res.body.status, 'OK', 'status is ok') 17 | t.end() 18 | }) 19 | }) 20 | 21 | tape('not found', function (t) { 22 | var url = '/404' 23 | servertest(server(), url, {encoding: 'json'}, function (err, res) { 24 | t.ifError(err, 'no error') 25 | 26 | t.equal(res.statusCode, 404, 'correct statusCode') 27 | t.equal(res.body.error, 'Resource Not Found', 'error match') 28 | t.end() 29 | }) 30 | }) 31 | 32 | tape('should get value', function (t) { 33 | var val = {some: 'test object'} 34 | Things.put('test-key', val, function (err) { 35 | t.ifError(err, 'no error') 36 | var url = '/things/get/test-key' 37 | servertest(server(), url, {encoding: 'json'}, function (err, res) { 38 | t.ifError(err, 'no error') 39 | 40 | t.equal(res.statusCode, 200, 'correct statusCode') 41 | t.deepEqual(res.body, val, 'values should match') 42 | t.end() 43 | }) 44 | }) 45 | }) 46 | 47 | tape('should put values', function (t) { 48 | var url = '/things/put/test-key2' 49 | var opts = { method: 'POST', encoding: 'json' } 50 | var val = {some: 'other test object'} 51 | 52 | servertest(server(), url, opts, onResponse) 53 | .end(JSON.stringify(val)) 54 | 55 | function onResponse (err, res) { 56 | t.ifError(err, 'no error') 57 | t.equal(res.statusCode, 200, 'correct statusCode') 58 | 59 | Things.get('test-key2', function (err, doc) { 60 | t.ifError(err, 'no error') 61 | t.deepEqual(doc, val) 62 | t.end() 63 | }) 64 | } 65 | }) 66 | 67 | tape('should get stream', function (t) { 68 | var url = '/things/stream/test-key/test-key3?format=ndjson' 69 | 70 | var expected = [ 71 | { some: 'test object' }, 72 | { some: 'other test object' } 73 | ] 74 | 75 | var lines = [] 76 | 77 | servertest(server(), url) 78 | .pipe(split()) 79 | .on('error', function (err) { t.ifError(err, 'no error') }) 80 | .on('data', function (line) { lines.push(JSON.parse(line)) }) 81 | .on('end', function () { 82 | t.deepEqual(lines, expected, 'response should match') 83 | t.end() 84 | }) 85 | }) 86 | 87 | tape('should get echo', function (t) { 88 | var url = '/echo?one=1&two=2' 89 | servertest(server(), url, {encoding: 'json'}, function (err, res) { 90 | t.ifError(err, 'no error') 91 | 92 | t.equal(res.statusCode, 200, 'correct statusCode') 93 | t.deepEqual(res.body, {one: '1', two: '2'}, 'values should match') 94 | t.end() 95 | }) 96 | }) 97 | 98 | tape('should get reverse', function (t) { 99 | var expected = { 100 | input: 'stringtoreverse', 101 | output: 'esreverotgnirts' 102 | } 103 | 104 | var url = '/reverse/' + expected.input 105 | servertest(server(), url, {encoding: 'json'}, function (err, res) { 106 | t.ifError(err, 'no error') 107 | 108 | t.equal(res.statusCode, 200, 'correct statusCode') 109 | t.deepEqual(res.body, expected, 'values should match') 110 | t.end() 111 | }) 112 | }) 113 | --------------------------------------------------------------------------------