├── .gitignore ├── .travis.yml ├── CHANGES.md ├── README.md ├── package.json ├── LICENSE ├── example └── example.js ├── test └── redmine.test.js └── lib └── redmine.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | example/my.example.js 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.6 4 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | CHANGES 2 | ========= 3 | 4 | 0.2.3 5 | ------- 6 | - Fixed bug of escaping JSON string. 7 | 8 | 0.2.2 9 | ------- 10 | - Fixed bug of escaping JSON string. 11 | 12 | 0.2.1 13 | ------- 14 | - Added getIssue() function. 15 | - Fixed bug due to unicode string. 16 | - Test improved. 17 | 18 | 19 | 0.2.0 20 | ------- 21 | - Replace eval() to JSON.parse(). 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-redmine 2 | =============== 3 | 4 | [![Build Status](https://secure.travis-ci.org/sotarok/node-redmine.png)](http://travis-ci.org/sotarok/node-redmine) 5 | 6 | Redmine REST API Client for node.js 7 | 8 | 9 | Features 10 | --------- 11 | 12 | * Issues 13 | 14 | (Only Issues API is available now.) 15 | 16 | 17 | Install 18 | --------- 19 | 20 | Install from npm: 21 | 22 | $ npm install redmine 23 | 24 | 25 | Link 26 | ------ 27 | 28 | * http://www.redmine.org/projects/redmine/wiki/Rest_api 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redmine", 3 | "version": "0.2.3", 4 | "description": "Redmine Rest API Client for node.js", 5 | "tags": ["redmine"], 6 | "keywords": ["redmine", "rest api"], 7 | "author": "Sotaro KARASAWA ", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/sotarok/node-redmine.git" 11 | }, 12 | "scripts": { 13 | "test": "TEST_REDMINE_APIKEY=e8a94353d2cbf45e302b08f6069327eeefa2cfb2 TEST_REDMINE_HOST=vivid-winter-3808.heroku.com node_modules/.bin/expresso test/*" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/sotarok/node-redmine/issues" 17 | }, 18 | "license": [ 19 | { 20 | "type": "MIT", 21 | "url": "https://github.com/sotarok/node-redmine/blob/master/LICENSE" 22 | } 23 | ], 24 | "devDependencies": { 25 | "expresso": ">=0.9.2" 26 | }, 27 | "main": "./lib/redmine" 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2012 Sotaro KARASAWA 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /example/example.js: -------------------------------------------------------------------------------- 1 | var Redmine = require('../lib/redmine'); 2 | 3 | var redmine = new Redmine({ 4 | host: 'redmine host', 5 | apiKey: 'redmine api key', 6 | }); 7 | 8 | 9 | // get issue 10 | redmine.getIssues({project_id: 1}, function(err, data) { 11 | if (err) { 12 | console.log("Error: " + err.message); 13 | return; 14 | } 15 | 16 | console.log("Issues:"); 17 | console.log(data); 18 | }); 19 | 20 | 21 | // create issue 22 | var issue = { 23 | project_id: 1, 24 | subject: "This is test issue on " + Date.now(), 25 | description: "Test issue description" 26 | }; 27 | redmine.postIssue(issue, function(err, data) { 28 | if (err) { 29 | console.log("Error: " + err.message); 30 | return; 31 | } 32 | 33 | console.log(data); 34 | }); 35 | 36 | 37 | // update issue 38 | var issueId = 4; // exist id 39 | var issueUpdate = { 40 | notes: "this is comment" 41 | }; 42 | redmine.updateIssue(issueId, issueUpdate, function(err, data) { 43 | if (err) { 44 | console.log("Error: " + err.message); 45 | return; 46 | } 47 | 48 | console.log(data); 49 | }); 50 | 51 | // delte issue 52 | var issueId = 4; 53 | redmine.deleteIssue(issueId, function(err, data) { 54 | if (err) { 55 | console.log("Error: " + err.message); 56 | return; 57 | } 58 | 59 | console.log(data); 60 | }); 61 | -------------------------------------------------------------------------------- /test/redmine.test.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var assert = require('assert'); 3 | var util = require('util'); 4 | 5 | var basedir = path.join(__dirname, '..'); 6 | var libdir = path.join(basedir, 'lib'); 7 | var assert = require('assert'); 8 | 9 | var Redmine = require(path.join(libdir, 'redmine.js')); 10 | 11 | assert.ok('TEST_REDMINE_APIKEY' in process.env); 12 | assert.ok('TEST_REDMINE_HOST' in process.env); 13 | 14 | var config = { 15 | host: process.env.TEST_REDMINE_HOST, 16 | apiKey: process.env.TEST_REDMINE_APIKEY 17 | }; 18 | 19 | var redmine = new Redmine(config); 20 | 21 | module.exports = { 22 | 'config': function(beforeExit, assert) 23 | { 24 | assert.ok(process.env.TEST_REDMINE_HOST == redmine.getHost()) 25 | assert.ok(process.env.TEST_REDMINE_APIKEY == redmine.getApiKey()) 26 | } 27 | ,'config error': function(beforeExit, assert) 28 | { 29 | try { 30 | new Redmine({}); 31 | } catch (e) { 32 | assert.type(e, 'object'); 33 | assert.includes(e.message, 'Error: apiKey and host must be configured.'); 34 | } 35 | } 36 | ,'get issue error': function(beforeExit, assert) 37 | { 38 | try { 39 | redmine.getIssue({foo: 1}, function(err, data) { 40 | }); 41 | } catch (e) { 42 | assert.type(e, 'object'); 43 | assert.includes(e.message, 'Error: Argument #1 id must be integer'); 44 | } 45 | } 46 | ,'get issue': function(beforeExit, assert) 47 | { 48 | redmine.getIssue(1, function(err, data) { 49 | assert.isNull(err, 'Err is null'); 50 | 51 | assert.type(data.issue, 'object', 'Data issue is an object'); 52 | 53 | var issue = data.issue; 54 | assert.equal(1, issue.id); 55 | assert.equal('Ticket1', issue.subject); 56 | }); 57 | } 58 | ,'get issues': function(beforeExit, assert) 59 | { 60 | redmine.getIssues({project_id: 1, limit: 2}, function(err, data) { 61 | assert.isNull(err, 'Err is null'); 62 | assert.equal(data.limit, 2); 63 | }); 64 | } 65 | ,'JSONStringify': function(beforeExit, assert) 66 | { 67 | var hoge = {hoge: 1}; 68 | assert.equal(Redmine.JSONStringify(hoge), '{"hoge":1}'); // plain JSON 69 | 70 | var hoge = {hoge: 'JSON with "escape string"'}; 71 | assert.equal(Redmine.JSONStringify(hoge), '{"hoge":"JSON with \\\"escape string\\\""}'); 72 | 73 | 74 | var hoge = {hoge: 'JSON with 日本語'}; 75 | var converted = Redmine.JSONStringify(hoge); 76 | assert.equal(converted, '{"hoge":"JSON with \\u65e5\\u672c\\u8a9e"}'); 77 | assert.eql(JSON.parse(converted), hoge); // invertible 78 | 79 | var hoge = {hoge: 'JSON with \n \r \b \\ \f \t '}; 80 | var converted = Redmine.JSONStringify(hoge); 81 | assert.equal(converted, '{"hoge":"JSON with \\n \\r \\b \\\\ \\f \\t "}'); 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /lib/redmine.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var util = require('util'); 3 | var url = require('url'); 4 | var querystring = require('querystring'); 5 | var util = require('util'); 6 | 7 | function escapeJSONString(key, value) { 8 | if (typeof value == 'string') { 9 | return value.replace(/[^ -~\b\t\n\f\r"\\]/g, function(a) { 10 | return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); 11 | }); 12 | } 13 | return value; 14 | } 15 | function JSONStringify(data) { 16 | return JSON.stringify(data, escapeJSONString).replace(/\\\\u([\da-f]{4}?)/g, '\\u$1'); 17 | } 18 | 19 | /** 20 | * Redmine 21 | */ 22 | function Redmine(config) { 23 | if (!config.apiKey || !config.host) { 24 | throw new Error("Error: apiKey and host must be configured."); 25 | } 26 | 27 | this.setApiKey(config.apiKey); 28 | this.setHost(config.host); 29 | } 30 | 31 | Redmine.prototype.version = '0.2.3'; 32 | 33 | Redmine.JSONStringify = JSONStringify; 34 | 35 | Redmine.prototype.setApiKey = function(key) { 36 | this.apiKey = key; 37 | }; 38 | 39 | Redmine.prototype.getApiKey = function() { 40 | return this.apiKey; 41 | }; 42 | 43 | Redmine.prototype.setHost = function(host) { 44 | this.host = host; 45 | }; 46 | 47 | Redmine.prototype.getHost = function() { 48 | return this.host; 49 | }; 50 | 51 | Redmine.prototype.generatePath = function(path, params) { 52 | if (path.slice(0, 1) != '/') { 53 | path = '/' + path; 54 | } 55 | return path + '?' + querystring.stringify(params); 56 | }; 57 | 58 | Redmine.prototype.request = function(method, path, params, callback) { 59 | if (!this.getApiKey() || !this.getHost()) { 60 | throw new Error("Error: apiKey and host must be configured."); 61 | } 62 | 63 | var options = { 64 | host: this.getHost(), 65 | path: method == 'GET' ? this.generatePath(path, params) : path, 66 | method: method, 67 | headers: { 68 | 'X-Redmine-API-Key': this.getApiKey() 69 | } 70 | }; 71 | 72 | var req = http.request(options, function(res) { 73 | //console.log('STATUS: ' + res.statusCode); 74 | //console.log('HEADERS: ' + JSON.stringify(res.headers)); 75 | 76 | if (res.statusCode != 200 && res.statusCode != 201) { 77 | callback({message: 'Server returns stats code: ' + res.statusCode, response: res}, null); 78 | callback = null; 79 | return ; 80 | } 81 | 82 | var body = ""; 83 | res.setEncoding('utf8'); 84 | res.on('data', function (chunk) { 85 | body += chunk; 86 | }); 87 | res.on('end', function(e) { 88 | var data = JSON.parse(body); 89 | callback(null, data); 90 | callback = null; 91 | }); 92 | }); 93 | 94 | req.on('error', function(err) { 95 | callback(err, null); 96 | callback = null; 97 | }); 98 | 99 | if (method != 'GET') { 100 | var body = JSONStringify(params); 101 | req.setHeader('Content-Length', body.length); 102 | req.setHeader('Content-Type', 'application/json'); 103 | req.write(body); 104 | } 105 | req.end(); 106 | }; 107 | 108 | /** 109 | * crud apis 110 | */ 111 | Redmine.prototype.getIssue = function(id, callback) { 112 | if (typeof id == 'integer') { 113 | throw new Error('Error: Argument #1 id must be integer'); 114 | } 115 | this.request('GET', '/issues/' + id + '.json', {}, callback); 116 | }; 117 | 118 | Redmine.prototype.getIssues = function(params, callback) { 119 | this.request('GET', '/issues.json', params, callback); 120 | }; 121 | 122 | Redmine.prototype.postIssue = function(params, callback) { 123 | this.request('POST', '/issues.json', {issue: params}, callback); 124 | }; 125 | 126 | Redmine.prototype.updateIssue = function(id, params, callback) { 127 | this.request('PUT', '/issues/' + id + '.json', {issue: params}, callback); 128 | }; 129 | 130 | Redmine.prototype.deleteIssue = function(id, callback) { 131 | this.request('DELETE', '/issues/' + id + '.json', {}, callback); 132 | }; 133 | 134 | 135 | /* 136 | * Exports 137 | */ 138 | module.exports = Redmine; 139 | --------------------------------------------------------------------------------