зеркало из https://github.com/mozilla/galaxy-api.git
THIS IS THE BIG BANG
This commit is contained in:
Родитель
12265262c2
Коммит
88790e90d2
|
@ -1,23 +1,19 @@
|
|||
lib-cov
|
||||
*.seed
|
||||
*.log
|
||||
*.csv
|
||||
*.dat
|
||||
*.floo*
|
||||
*.gz
|
||||
*.log
|
||||
*.out
|
||||
*.pid
|
||||
*.gz
|
||||
*.floo*
|
||||
|
||||
pids
|
||||
logs
|
||||
results
|
||||
*.seed
|
||||
build
|
||||
|
||||
node_modules
|
||||
|
||||
settings_local.js
|
||||
settings_test.js
|
||||
scripts/*.json
|
||||
data/
|
||||
dump.rdb
|
||||
static/uploads
|
||||
lib-cov
|
||||
logs
|
||||
node_modules
|
||||
pids
|
||||
results
|
||||
scripts/*.json
|
||||
settings_local.js
|
||||
settings_test.js
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
# galaxy-api
|
||||
|
||||
Here lies the REST API for [Galaxy](https://github.com/mozilla/galaxy).
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
To install dependencies:
|
||||
|
||||
npm install
|
||||
|
||||
Node 0.11.x is required for the `--harmony` flag which enables generators. If you're running an earlier version of Node you may install [n](https://github.com/visionmedia/n), a node version manager to quickly install 0.11.x:
|
||||
|
||||
npm install -g n
|
||||
n 0.11.12
|
||||
|
||||
|
||||
## Development
|
||||
|
||||
To run the local web server:
|
||||
|
||||
nodemon --harmony bin/api
|
||||
|
||||
Alternatively:
|
||||
|
||||
npm run-script dev
|
||||
|
||||
|
||||
## Testing
|
||||
|
||||
npm test
|
||||
|
||||
|
||||
## Deployment
|
||||
|
||||
To run the local web server:
|
||||
|
||||
node --harmony bin/api
|
||||
|
||||
Alternatively:
|
||||
|
||||
npm start
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "games",
|
||||
"description": "games",
|
||||
"routes": {
|
||||
"GET /games": "all",
|
||||
"POST /games": "create",
|
||||
"GET /games/:slug": "get",
|
||||
"PATCH /games/:slug": "update",
|
||||
"PUT /games/:slug": "replace",
|
||||
"DELETE /games/:slug": "delete"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,199 @@
|
|||
/**
|
||||
* See routes defined in `config.json`.
|
||||
*/
|
||||
|
||||
var Joi = require('joi');
|
||||
var Promise = require('es6-promise').Promise;
|
||||
|
||||
var db = require('../../lib/db');
|
||||
var utils = require('../../lib/utils');
|
||||
|
||||
var games = {}; // Local cache of game data
|
||||
var redis = db.redis(); // Real database for game data
|
||||
var validate = utils.promisify(Joi.validate); // Promise-based `Joi.validate`
|
||||
|
||||
|
||||
var gameKeys = {
|
||||
// App URL must start with `https://` or `http://`.
|
||||
app_url: Joi.string().regex(/^https?:\/\//).required()
|
||||
.example('http://nintendo.com/mario-bros/'),
|
||||
|
||||
// App Name cannot be longer than 150 characters long.
|
||||
name: Joi.string().max(150).required()
|
||||
.example('Mario Bros.'),
|
||||
|
||||
// App Slug cannot be all digits, all underscores, or all hyphens
|
||||
// and must contain only letters, numbers, underscores, and hyphens.
|
||||
// TODO: Throw an error if `slug` is already taken.
|
||||
slug: Joi.string().regex(/^(?!\d*$)(?!_*$)(?!-*$)[\w-]+$/).required()
|
||||
.example('mario-bros')
|
||||
};
|
||||
|
||||
// Define schema for JSON payloads. (Run `Joi.describe` to see examples.)
|
||||
var gameSchema = Joi.object().keys(gameKeys).example({
|
||||
app_url: 'http://nintendo.com/mario-bros/',
|
||||
name: 'Mario Bros.',
|
||||
slug: 'mario-bros'
|
||||
});
|
||||
|
||||
|
||||
// For PATCH, use the same schema except every field is optional.
|
||||
var gameKeysPatch = {};
|
||||
Object.keys(gameKeys).forEach(function (key) {
|
||||
gameKeysPatch[key] = gameKeys[key].optional();
|
||||
});
|
||||
|
||||
var gameSchemaPatch = Joi.object().keys(gameKeysPatch);
|
||||
|
||||
|
||||
/**
|
||||
* GET all games.
|
||||
*/
|
||||
exports.all = function *() {
|
||||
var response = new utils.Response(this);
|
||||
|
||||
yield redis.hvals('game')
|
||||
.then(function (values) {
|
||||
// `values` is an array of serialised games.
|
||||
// Create an array of parsed games, and update the local cache.
|
||||
games = values.map(JSON.parse);
|
||||
|
||||
// Return 200 with an array of all the games.
|
||||
// TODO: Return an object with paginated results and metadata.
|
||||
response.success(games);
|
||||
}, response.dbError);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* POST a new game.
|
||||
*/
|
||||
exports.create = function *() {
|
||||
var payload = this.request.body;
|
||||
var response = new utils.Response(this);
|
||||
|
||||
yield validate(payload, gameSchema, {abortEarly: false})
|
||||
.then(function () {
|
||||
return redis.hset('game', payload.slug, JSON.stringify(payload))
|
||||
.then(function () {
|
||||
response.success();
|
||||
|
||||
// Add game to local cache.
|
||||
games[payload.slug] = payload;
|
||||
},
|
||||
response.dbError);
|
||||
},
|
||||
response.validationError);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* GET a single game.
|
||||
*/
|
||||
exports.get = function *() {
|
||||
var response = new utils.Response(this);
|
||||
var slug = this.params.slug;
|
||||
|
||||
var game = yield redis.hget('game', slug).catch(response.dbError);
|
||||
|
||||
if (game) {
|
||||
// Return 200 with game data.
|
||||
response.success(game);
|
||||
} else if (game === null) {
|
||||
// Return 404 if game slug does not exist as a key in the database.
|
||||
response.missing(game);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function* edit(self, replace) {
|
||||
var payload = self.request.body;
|
||||
var response = new utils.Response(self);
|
||||
|
||||
var oldSlug = self.params.slug;
|
||||
var newSlug = 'slug' in payload ? payload.slug : oldSlug;
|
||||
var newGameData;
|
||||
var schema = replace ? gameSchema : gameSchemaPatch;
|
||||
|
||||
function editSuccess() {
|
||||
response.success();
|
||||
|
||||
// Update game in local cache.
|
||||
games[payload.slug] = payload;
|
||||
}
|
||||
|
||||
yield validate(payload, schema, {abortEarly: false})
|
||||
.then(function () {
|
||||
return redis.hget('game', oldSlug)
|
||||
.then(function (gameData) {
|
||||
if (gameData === null) {
|
||||
return response.missing();
|
||||
}
|
||||
|
||||
if (replace) {
|
||||
newGameData = JSON.stringify(payload);
|
||||
} else {
|
||||
// Deserialise.
|
||||
newGameData = JSON.parse(gameData);
|
||||
|
||||
// Replace the old keys' values with the new keys' values.
|
||||
Object.keys(payload).forEach(function (key) {
|
||||
newGameData[key] = payload[key];
|
||||
});
|
||||
|
||||
// Serialise.
|
||||
newGameData = JSON.stringify(newGameData);
|
||||
}
|
||||
|
||||
return redis.hset('game', newSlug, newGameData)
|
||||
.then(function () {
|
||||
if (newSlug !== oldSlug) {
|
||||
// Slug was changed, so rename keys.
|
||||
delete games[oldSlug];
|
||||
return redis.hdel('game', oldSlug).then(editSuccess);
|
||||
}
|
||||
|
||||
return editSuccess();
|
||||
});
|
||||
});
|
||||
},
|
||||
response.validationError).catch(response.dbError);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* PATCH a single game (change only the fields supplied).
|
||||
*/
|
||||
exports.update = function *() {
|
||||
yield edit(this);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* PUT a single game (replace the entire object).
|
||||
*/
|
||||
exports.replace = function *() {
|
||||
yield edit(this, true);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* DELETE a single game.
|
||||
*/
|
||||
exports.delete = function *() {
|
||||
var slug = this.params.slug;
|
||||
|
||||
if (slug in games) {
|
||||
delete games[slug];
|
||||
}
|
||||
|
||||
var game = yield redis.hdel('game', slug).catch(response.dbError);
|
||||
|
||||
if (game) {
|
||||
// Return 200 with success.
|
||||
response.success();
|
||||
} else if (game === null) {
|
||||
// Return 404 if game slug does not exist as a key in the database.
|
||||
response.missing(game);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,75 @@
|
|||
var request = require('supertest');
|
||||
|
||||
var api = require('../..');
|
||||
|
||||
|
||||
describe('GET /games', function () {
|
||||
it('should respond with games', function (done) {
|
||||
var app = api();
|
||||
|
||||
request(app.listen())
|
||||
.get('/games')
|
||||
.expect(200)
|
||||
.end(done);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('POST /games/', function () {
|
||||
it('should respond with a success message', function (done) {
|
||||
var app = api();
|
||||
|
||||
request(app.listen())
|
||||
.post('/games')
|
||||
.expect(200)
|
||||
.end(done);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('GET /games/:slug', function () {
|
||||
it('should respond with a single game', function (done) {
|
||||
var app = api();
|
||||
|
||||
request(app.listen())
|
||||
.get('/games/mario-bros')
|
||||
.expect(200)
|
||||
.end(done);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('PATCH /games/:slug', function () {
|
||||
it('should respond with a success message', function (done) {
|
||||
var app = api();
|
||||
|
||||
request(app.listen())
|
||||
.patch('/games/mario-bros')
|
||||
.expect(200)
|
||||
.end(done);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('PUT /games/:slug', function () {
|
||||
it('should respond with a success message', function (done) {
|
||||
var app = api();
|
||||
|
||||
request(app.listen())
|
||||
.put('/games/mario-bros')
|
||||
.expect(200)
|
||||
.end(done);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('DELETE /games/:slug', function () {
|
||||
it('should respond with a success message', function (done) {
|
||||
var app = api();
|
||||
|
||||
request(app.listen())
|
||||
.delete('/games/mario-bros')
|
||||
.expect(200)
|
||||
.end(done);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,36 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var program = require('commander');
|
||||
|
||||
var api = require('..');
|
||||
|
||||
|
||||
// Options.
|
||||
program
|
||||
.option('-H, --host <host>', 'specify the host [$HOST || 0.0.0.0]',
|
||||
process.env.HOST || '0.0.0.0')
|
||||
.option('-p, --port <port>', 'specify the port [$PORT || 4000]',
|
||||
process.env.PORT || '4000')
|
||||
.option('-b, --backlog <size>', 'specify the backlog size [511]',
|
||||
process.env.BACKLOG || '511')
|
||||
.option('-r, --ratelimit <n>', 'ratelimit requests [2500]',
|
||||
process.env.RATELIMIT || '2500')
|
||||
.option('-d, --ratelimit-duration <ms>', 'ratelimit duration [1h]',
|
||||
process.env.RATELIMIT_DURATION || '1h')
|
||||
.parse(process.argv);
|
||||
|
||||
|
||||
// Create app.
|
||||
var app = api({
|
||||
ratelimit: ~~program.ratelimit,
|
||||
duration: ~~program.ratelimitDuration
|
||||
});
|
||||
|
||||
|
||||
// Listen.
|
||||
app.listen(program.port, program.host, ~~program.backlog);
|
||||
console.log('Listening on %s:%s', program.host, program.port);
|
|
@ -0,0 +1,70 @@
|
|||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var body = require('koa-parse-json');
|
||||
var compress = require('koa-compress');
|
||||
var koa = require('koa');
|
||||
var logger = require('koa-logger');
|
||||
var ratelimit = require('koa-ratelimit');
|
||||
var responseTime = require('koa-response-time');
|
||||
var router = require('koa-router');
|
||||
|
||||
var redis = require('redis');
|
||||
|
||||
var load = require('./lib/load');
|
||||
|
||||
|
||||
/**
|
||||
* Environment.
|
||||
*/
|
||||
var env = process.env.NODE_ENV || 'development';
|
||||
|
||||
|
||||
/**
|
||||
* Expose `api()`.
|
||||
*/
|
||||
module.exports = api;
|
||||
|
||||
|
||||
/**
|
||||
* Initialise an app with the given `opts`.
|
||||
*
|
||||
* @param {Object} opts
|
||||
* @return {Application}
|
||||
* @api public
|
||||
*/
|
||||
function api(opts) {
|
||||
opts = opts || {};
|
||||
|
||||
var app = koa();
|
||||
|
||||
// Logging.
|
||||
if (env !== 'test') {
|
||||
app.use(logger());
|
||||
}
|
||||
|
||||
// X-Response-Time.
|
||||
app.use(responseTime());
|
||||
|
||||
// Compression.
|
||||
app.use(compress());
|
||||
|
||||
// Rate limiting.
|
||||
app.use(ratelimit({
|
||||
max: opts.ratelimit,
|
||||
duration: opts.duration,
|
||||
db: redis.createClient()
|
||||
}));
|
||||
|
||||
// Parse JSON bodies.
|
||||
app.use(body());
|
||||
|
||||
// Routing.
|
||||
app.use(router(app));
|
||||
|
||||
// Boot.
|
||||
load(app, __dirname + '/api');
|
||||
|
||||
return app;
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
var urllib = require('url');
|
||||
|
||||
var redis = require('romis');
|
||||
|
||||
|
||||
var redisURL = urllib.parse(process.env.REDIS_URL || '');
|
||||
|
||||
|
||||
/**
|
||||
* Establish connection to Redis database.
|
||||
*
|
||||
* You must first define an environment variable, like so:
|
||||
*
|
||||
* export REDIS_URL='redis://[db-number[:password]@]host:port'
|
||||
* export REDIS_URL='redis://[[:password]@]host:port/[db-number]'
|
||||
*
|
||||
* All fields after the scheme are optional and will default to
|
||||
* `localhost` on port `6379`, using database `0`.
|
||||
*
|
||||
* This is a good default:
|
||||
*
|
||||
* export REDIS_URL='redis://localhost:6379'
|
||||
*
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
module.exports.redis = function () {
|
||||
var client = redis.createClient(parseInt(redisURL.port || '6379', 10),
|
||||
redisURL.hostname || 'localhost');
|
||||
|
||||
var redisAuth = (redisURL.auth || '').split(':');
|
||||
var db = redisAuth[0];
|
||||
var passwd = redisAuth[1];
|
||||
|
||||
if (passwd = redisAuth[1]) {
|
||||
client.auth(passwd, function (err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!db && redisURL.pathname && redisURL.pathname !== '/') {
|
||||
db = redisURL.pathname.substring(1);
|
||||
}
|
||||
|
||||
if (db) {
|
||||
client.select(db);
|
||||
client.on('connect', function () {
|
||||
redis.send_anyways = true;
|
||||
redis.select(db);
|
||||
redis.send_anyways = false;
|
||||
});
|
||||
}
|
||||
|
||||
return client;
|
||||
};
|
|
@ -0,0 +1,58 @@
|
|||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var join = path.resolve;
|
||||
var readdir = fs.readdirSync;
|
||||
|
||||
var debug = require('debug')('api');
|
||||
|
||||
|
||||
/**
|
||||
* Load resources in `root` directory.
|
||||
*
|
||||
* @param {Application} app
|
||||
* @param {String} root
|
||||
* @api private
|
||||
*/
|
||||
module.exports = function (app, root) {
|
||||
readdir(root).forEach(function (file) {
|
||||
var dir = join(root, file);
|
||||
var stats = fs.lstatSync(dir);
|
||||
if (stats.isDirectory()) {
|
||||
var conf = require(dir + '/config.json');
|
||||
conf.name = file;
|
||||
conf.directory = dir;
|
||||
if (conf.routes) {
|
||||
route(app, conf);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Define routes in `conf`.
|
||||
*/
|
||||
function route(app, conf) {
|
||||
debug('routes: %s', conf.name);
|
||||
|
||||
var mod = require(conf.directory);
|
||||
|
||||
Object.keys(conf.routes).forEach(function (key) {
|
||||
var prop = conf.routes[key];
|
||||
var chunks = key.split(' ');
|
||||
var method = chunks[0];
|
||||
var path = chunks[1];
|
||||
debug('%s %s -> .%s', method, path, prop);
|
||||
|
||||
var fn = mod[prop];
|
||||
if (!fn) {
|
||||
throw new Error(conf.name + ': exports.' + prop + ' is not defined');
|
||||
}
|
||||
|
||||
app[method.toLowerCase()](path, fn);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
var Promise = require('es6-promise').Promise;
|
||||
|
||||
|
||||
module.exports.promisify = function (func) {
|
||||
return function () {
|
||||
var args = Array.prototype.slice.apply(arguments);
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
func.apply({}, args.concat(function (err, value) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(value);
|
||||
}
|
||||
}));
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
module.exports.Response = function (self) {
|
||||
return {
|
||||
success: function (body) {
|
||||
// Return 200 with success.
|
||||
self.status = 200;
|
||||
self.body = body || {success: true};
|
||||
},
|
||||
validationError: function (err) {
|
||||
// Return 400 for validation error.
|
||||
self.status = 400;
|
||||
self.body = err;
|
||||
},
|
||||
missing: function () {
|
||||
// Return 404 if game slug does not exist as a key in the database.
|
||||
self.status = 404;
|
||||
self.body = {error: 'not_found'};
|
||||
},
|
||||
dbError: function (err) {
|
||||
console.error('DB error: ' + err);
|
||||
// Return 500 for database error.
|
||||
self.status = 500;
|
||||
self.body = {error: 'db_error'};
|
||||
}
|
||||
};
|
||||
};
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"name": "galaxy-api",
|
||||
"description": "Galaxy API",
|
||||
"dependencies": {
|
||||
"commander": "~2.2.0",
|
||||
"debug": "^1.0.4",
|
||||
"joi": "^4.6.2",
|
||||
"koa": "^0.8.2",
|
||||
"koa-compress": "1.0.7",
|
||||
"koa-logger": "~1.2.1",
|
||||
"koa-parse-json": "^1.0.0",
|
||||
"koa-ratelimit": "~1.0.3",
|
||||
"koa-response-time": "~1.0.2",
|
||||
"koa-router": "~3.1.4",
|
||||
"romis": "^0.0.4",
|
||||
"es6-promise": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^1.9.1",
|
||||
"mocha": "^1.21.3",
|
||||
"should": "^4.0.4",
|
||||
"supertest": "^0.13.0",
|
||||
"nodemon": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.11.11",
|
||||
"npm": ">=1.1.x"
|
||||
},
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/mozilla/galaxy-api.git"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node --harmony ./bin/api",
|
||||
"dev": "nodemon --harmony ./bin/api",
|
||||
"test": "NODE_ENV=test ./node_modules/.bin/mocha --require should --reporter spec --harmony --bail api/*/test.js"
|
||||
},
|
||||
"version": "0.0.3"
|
||||
}
|
Загрузка…
Ссылка в новой задаче