├── .gitignore ├── LICENSE ├── README.md ├── app.js ├── bin └── www ├── package.json ├── queue └── payments.js ├── routes └── payments.js └── test └── payments.js /.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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Mark Thomas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redis-kue-express 2 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | const path = require('path'); 5 | const favicon = require('serve-favicon'); 6 | const logger = require('morgan'); 7 | const kue = require('kue'); 8 | const cookieParser = require('cookie-parser'); 9 | const bodyParser = require('body-parser'); 10 | 11 | const payments = require('./routes/payments'); 12 | 13 | const app = express(); 14 | 15 | 16 | // uncomment after placing your favicon in /public 17 | //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); 18 | app.use(logger('dev')); 19 | app.use(bodyParser.json()); 20 | app.use(bodyParser.urlencoded({ extended: false })); 21 | app.use(cookieParser()); 22 | app.use(express.static(path.join(__dirname, 'public'))); 23 | 24 | app.use('/queue', kue.app); 25 | app.use('/payments', payments); 26 | 27 | // catch 404 and forward to error handler 28 | app.use((req, res, next) => { 29 | let err = new Error('Not Found'); 30 | err.status = 404; 31 | next(err); 32 | }); 33 | 34 | // error handlers 35 | 36 | // development error handler 37 | // will print stacktrace 38 | if (app.get('env') === 'development') { 39 | app.use((err, req, res, next) => { 40 | res.status(err.status || 500); 41 | res.json({ 42 | message: err.message, 43 | error: err 44 | }); 45 | }); 46 | } 47 | 48 | // production error handler 49 | // no stacktraces leaked to user 50 | app.use((err, req, res, next) => { 51 | res.status(err.status || 500); 52 | res.json({ 53 | message: err.message, 54 | error: {} 55 | }); 56 | }); 57 | 58 | 59 | module.exports = app; 60 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | const app = require('../app'); 8 | const debug = require('debug')('redis-kue-express:server'); 9 | const http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | const port = normalizePort(process.env.PORT || '3000'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | const server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | var addr = server.address(); 86 | var bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redis-kue-express", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www", 7 | "test": "node test/*.js" 8 | }, 9 | "dependencies": { 10 | "body-parser": "~1.13.2", 11 | "cookie-parser": "~1.3.5", 12 | "debug": "~2.2.0", 13 | "express": "~4.13.3", 14 | "kue": "^0.10.2", 15 | "morgan": "~1.6.1", 16 | "serve-favicon": "~2.3.0" 17 | }, 18 | "devDependencies": { 19 | "supertest": "^1.1.0", 20 | "tape": "^4.2.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /queue/payments.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let redisConfig; 4 | if (process.env.NODE_ENV === 'production') { 5 | redisConfig = { 6 | redis: { 7 | port: process.env.REDIS_PORT, 8 | host: process.env.REDIS_HOST, 9 | auth: process.env.REDIS_PASS, 10 | options: { 11 | no_ready_check: false 12 | } 13 | } 14 | }; 15 | } else { 16 | redisConfig = {}; 17 | } 18 | 19 | const kue = require('kue'); 20 | const queue = kue.createQueue(redisConfig); 21 | queue.watchStuckJobs(1000 * 10); 22 | 23 | queue.on('ready', () => { 24 | console.info('Queue is ready!'); 25 | }); 26 | 27 | queue.on('error', (err) => { 28 | console.error('There was an error in the main queue!'); 29 | console.error(err); 30 | console.error(err.stack); 31 | }); 32 | 33 | 34 | // Set up UI 35 | kue.app.listen(process.env.KUE_PORT); 36 | kue.app.set('title', 'Kue'); 37 | 38 | function createPayment(data, done) { 39 | queue.create('payment', data) 40 | .priority('critical') 41 | .attempts(8) 42 | .backoff(true) 43 | .removeOnComplete(false) 44 | .save(err => { 45 | if (err) { 46 | console.error(err); 47 | done(err); 48 | } 49 | if (!err) { 50 | done(); 51 | } 52 | }); 53 | } 54 | 55 | // Process up to 20 jobs concurrently 56 | queue.process('payment', 20, function(job, done){ 57 | // other processing work here 58 | // ... 59 | // ... 60 | 61 | // Call done when finished 62 | done(); 63 | }); 64 | 65 | module.exports = { 66 | create: (data, done) => { 67 | createPayment(data, done); 68 | } 69 | }; -------------------------------------------------------------------------------- /routes/payments.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const router = require('express').Router(); 4 | const payment = require('../queue/payments'); 5 | 6 | router.post('/', (req, res, next) => { 7 | const order = req.body; 8 | payment.create(order, (err) => { 9 | if (err) { 10 | return res.json({ 11 | error: err, 12 | success: false, 13 | message: 'Could not create payment', 14 | }); 15 | } else { 16 | return res.json({ 17 | error: null, 18 | success: true, 19 | message: 'Successfully created payment', 20 | order 21 | }); 22 | } 23 | }) 24 | }); 25 | 26 | module.exports = router; -------------------------------------------------------------------------------- /test/payments.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const supertest = require('supertest'); 4 | const app = require('../app'); 5 | const api = supertest(app); 6 | const queue = require('kue').createQueue(); 7 | 8 | const test = require('tape'); 9 | 10 | const dummyOrder = { 11 | // This job property lets you make better use of the UI 12 | title: 'Order #4kSvjL_Qx', 13 | paymentToken: '4kSvjL_Qx', 14 | orderID: '1a2b3c4', 15 | received: true, 16 | receivedAt: new Date('December 24, 2015 23:59:59'), 17 | createdAt: new Date('December 24, 2015 23:58:59'), 18 | productID: '5d6e6f', 19 | customer: { 20 | firstName: 'A', 21 | lastName: 'Person', 22 | email: 'example@example.com', 23 | address: '1234 somewhere lane, ? USA 12345' 24 | } 25 | }; 26 | 27 | 28 | test('Receiving and processing payments', t => { 29 | api 30 | .post('/payments') 31 | .send(dummyOrder) 32 | .end((err, res) => { 33 | const order = res.body.order 34 | 35 | // Check for response body 36 | t.ok(res.body, 'Should respond with a body'); 37 | 38 | // Check for response meta properties 39 | t.equals(res.body.success, true, 'The success property should be true'); 40 | t.equals(res.body.error, null, 'The error property should be null'); 41 | t.ok(res.body.message, 'Should have a message property'); 42 | 43 | // Check to see if the order is intact 44 | t.equals(order.received, true, 'Should have been received'); 45 | t.equals(order.orderID, dummyOrder.orderID, 'Order ID should be the same'); 46 | t.equals(order.paymentToken, dummyOrder.paymentToken, 'Payment token should be the same'); 47 | t.equals(order.productID, dummyOrder.productID, 'Product ID should be the same'); 48 | t.end(); 49 | }); 50 | }); 51 | 52 | test('Creating payments and processing items with the queue', t => { 53 | queue.testMode.enter(); 54 | 55 | queue.createJob('payment', dummyOrder).save(); 56 | queue.createJob('payment', dummyOrder).save(); 57 | 58 | t.equal(queue.testMode.jobs.length, 2, 'There should be two jobs'); 59 | t.equal(queue.testMode.jobs[0].type, 'payment', 'The jobs should be of type payment'); 60 | t.equal(queue.testMode.jobs[0].data, dummyOrder, 'The job data should be intact'); 61 | 62 | queue.testMode.clear(); 63 | queue.testMode.exit() 64 | t.end(); 65 | }); --------------------------------------------------------------------------------