Merge pull request #6256 from HabitRPG/api-v3-apidoc
API v3 - apidoc wip
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
.DS_Store
|
||||
website/public/gen
|
||||
website/public/common
|
||||
website/public/apidoc
|
||||
node_modules
|
||||
*.swp
|
||||
.idea*
|
||||
|
||||
+10
-1
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"name": "habitrpg",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "0.0.0-152",
|
||||
"version": "3.0.0-alpha",
|
||||
"main": "./website/src/index.js",
|
||||
"dependencies": {
|
||||
"accepts": "^1.3.0",
|
||||
"amazon-payments": "0.0.4",
|
||||
"amplitude": "^2.0.3",
|
||||
"apidoc": "^0.13.1",
|
||||
"async": "^1.5.0",
|
||||
"aws-sdk": "^2.0.25",
|
||||
"babel-core": "^5.8.34",
|
||||
@@ -119,6 +120,7 @@
|
||||
"mongodb": "^2.0.46",
|
||||
"mongoskin": "~0.6.1",
|
||||
"nock": "^2.17.0",
|
||||
"phantomjs": "^1.9.18",
|
||||
"protractor": "~2.5.1",
|
||||
"rewire": "^2.3.3",
|
||||
"rimraf": "^2.4.3",
|
||||
@@ -130,5 +132,12 @@
|
||||
"uuid": "^2.0.1",
|
||||
"vinyl-source-stream": "^1.0.0",
|
||||
"vinyl-transform": "^1.0.0"
|
||||
},
|
||||
"apidoc": {
|
||||
"name": "habitica",
|
||||
"title": "Habitica",
|
||||
"version": "3.0.0",
|
||||
"url": "https://habitica.com/api/v3",
|
||||
"sampleUrl": "https://habitica.com/api/v3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import gulp from 'gulp';
|
||||
import clean from 'rimraf';
|
||||
import apidoc from 'apidoc';
|
||||
|
||||
const APIDOC_DEST_PATH = './website/public/apidoc';
|
||||
const APIDOC_SRC_PATH = './website/src';
|
||||
gulp.task('apidoc:clean', (done) => {
|
||||
clean(APIDOC_DEST_PATH, done);
|
||||
});
|
||||
|
||||
gulp.task('apidoc', ['apidoc:clean'], (done) => {
|
||||
let result = apidoc.createDoc({
|
||||
src: APIDOC_SRC_PATH,
|
||||
dest: APIDOC_DEST_PATH,
|
||||
});
|
||||
|
||||
if (result === false) {
|
||||
done(new Error('There was a problem generating apiDoc documentation.'))
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
+1
-1
@@ -17,6 +17,6 @@ gulp.task('build:dev:watch', ['build:dev'], () => {
|
||||
gulp.watch(['website/public/**/*.styl', 'common/script/*']);
|
||||
});
|
||||
|
||||
gulp.task('build:prod', ['browserify', 'prepare:staticNewStuff'], (done) => {
|
||||
gulp.task('build:prod', ['browserify', 'prepare:staticNewStuff', 'apidoc'], (done) => {
|
||||
gulp.start('grunt-build:prod', done);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
NotAuthorized,
|
||||
BadRequest,
|
||||
InternalServerError,
|
||||
NotFound,
|
||||
} from '../../../../../website/src/libs/api-v3/errors';
|
||||
|
||||
describe('Custom Errors', () => {
|
||||
@@ -21,7 +22,7 @@ describe('Custom Errors', () => {
|
||||
expect(notAuthorizedError).to.be.an.instanceOf(CustomError);
|
||||
});
|
||||
|
||||
it('it returns an http code of 400', () => {
|
||||
it('it returns an http code of 401', () => {
|
||||
let notAuthorizedError = new NotAuthorized();
|
||||
|
||||
expect(notAuthorizedError.httpCode).to.eql(401);
|
||||
@@ -40,6 +41,32 @@ describe('Custom Errors', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('NotFound', () => {
|
||||
it('is an instance of CustomError', () => {
|
||||
let notAuthorizedError = new NotFound();
|
||||
|
||||
expect(notAuthorizedError).to.be.an.instanceOf(CustomError);
|
||||
});
|
||||
|
||||
it('it returns an http code of 404', () => {
|
||||
let notAuthorizedError = new NotFound();
|
||||
|
||||
expect(notAuthorizedError.httpCode).to.eql(404);
|
||||
});
|
||||
|
||||
it('returns a default message', () => {
|
||||
let notAuthorizedError = new NotFound();
|
||||
|
||||
expect(notAuthorizedError.message).to.eql('Not found.');
|
||||
});
|
||||
|
||||
it('allows a custom message', () => {
|
||||
let notAuthorizedError = new NotFound('Custom Error Message');
|
||||
|
||||
expect(notAuthorizedError.message).to.eql('Custom Error Message');
|
||||
});
|
||||
});
|
||||
|
||||
describe('BadRequest', () => {
|
||||
it('is an instance of CustomError', () => {
|
||||
let badRequestError = new BadRequest();
|
||||
@@ -82,7 +109,7 @@ describe('Custom Errors', () => {
|
||||
it('returns a default message', () => {
|
||||
let internalServerError = new InternalServerError();
|
||||
|
||||
expect(internalServerError.message).to.eql('Internal server error.');
|
||||
expect(internalServerError.message).to.eql('An unexpected error occurred.');
|
||||
});
|
||||
|
||||
it('allows a custom message', () => {
|
||||
|
||||
@@ -31,7 +31,7 @@ describe('errorHandler', () => {
|
||||
expect(res.status).to.be.calledWith(500);
|
||||
expect(res.json).to.be.calledWith({
|
||||
error: 'InternalServerError',
|
||||
message: 'Internal server error.',
|
||||
message: 'An unexpected error occurred.',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -63,7 +63,7 @@ describe('errorHandler', () => {
|
||||
expect(res.status).to.be.calledWith(500);
|
||||
expect(res.json).to.be.calledWith({
|
||||
error: 'InternalServerError',
|
||||
message: 'Internal server error.',
|
||||
message: 'An unexpected error occurred.',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -88,7 +88,7 @@ describe('errorHandler', () => {
|
||||
errorHandler(error, req, res, next);
|
||||
|
||||
expect(logger.error).to.be.calledOnce;
|
||||
expect(logger.error).to.be.calledWith(error.stack, {
|
||||
expect(logger.error).to.be.calledWithExactly(error.stack, {
|
||||
originalUrl: req.originalUrl,
|
||||
headers: req.headers,
|
||||
body: req.body,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { requester } from '../../../../helpers/api-integration.helper';
|
||||
|
||||
describe('notFound Middleware', () => {
|
||||
it('returns a 404 error when the resource is not found', () => {
|
||||
let request = requester().get('/api/v3/dummy-url');
|
||||
|
||||
return request.then((errBody) => {
|
||||
expect(errBody.error).to.equal('NotFound');
|
||||
expect(errBody.message).to.equal('Not found.');
|
||||
}).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,15 +1,34 @@
|
||||
// An example file to show how a controller should be structured
|
||||
let api = {};
|
||||
|
||||
/**
|
||||
* @api {get} /example/:id Request Example information
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName GetExample
|
||||
* @apiGroup Example
|
||||
*
|
||||
* @apiParam {Number} id Examples unique ID.
|
||||
*
|
||||
* @apiSuccess {String} firstname Firstname of the Example.
|
||||
* @apiSuccess {String} lastname Lastname of the Example.
|
||||
*
|
||||
* @apiSuccessExample Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "firstname": "John",
|
||||
* "lastname": "Doe"
|
||||
* }
|
||||
*
|
||||
* @apiUse NotFound
|
||||
*/
|
||||
api.exampleRoute = {
|
||||
method: 'GET',
|
||||
url: '/example/:param',
|
||||
url: '/example/:id',
|
||||
middlewares: [],
|
||||
handler (req, res) {
|
||||
res.status(200).send({
|
||||
status: 'ok',
|
||||
status: req.params.id,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default api;
|
||||
export default api;
|
||||
|
||||
@@ -14,8 +14,6 @@ var IS_PROD = nconf.get('IS_PROD');
|
||||
var IS_DEV = nconf.get('IS_DEV');
|
||||
var cores = Number(nconf.get('WEB_CONCURRENCY')) || 0;
|
||||
|
||||
if (IS_DEV) Error.stackTraceLimit = Infinity;
|
||||
|
||||
// Setup the cluster module
|
||||
if (cores !== 0 && cluster.isMaster && (IS_DEV || IS_PROD)) {
|
||||
// Fork workers. If config.json has CORES=x, use that - otherwise, use all cpus-1 (production)
|
||||
|
||||
@@ -7,8 +7,17 @@ export class CustomError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
// NotAuthorized error with a 401 http error code
|
||||
// used when a request is not authorized
|
||||
/**
|
||||
* @apiDefine NotFound
|
||||
* @apiError NotFound The client is not authorized to make this request.
|
||||
*
|
||||
* @apiErrorExample Error-Response:
|
||||
* HTTP/1.1 401 Unauthorized
|
||||
* {
|
||||
* "error": "NotAuthorized",
|
||||
* "message": "Not authorized."
|
||||
* }
|
||||
*/
|
||||
export class NotAuthorized extends CustomError {
|
||||
constructor (customMessage) {
|
||||
super();
|
||||
@@ -18,9 +27,17 @@ export class NotAuthorized extends CustomError {
|
||||
}
|
||||
}
|
||||
|
||||
// BadRequest error with a 400 http error code
|
||||
// used for requests not formatted correctly
|
||||
// TODO use for validation errors too?
|
||||
/**
|
||||
* @apiDefine BadRequest
|
||||
* @apiError BadRequest The request wasn't formatted correctly.
|
||||
*
|
||||
* @apiErrorExample Error-Response:
|
||||
* HTTP/1.1 400 Bad Request
|
||||
* {
|
||||
* "error": "BadRequest",
|
||||
* "message": "Bad request."
|
||||
* }
|
||||
*/
|
||||
export class BadRequest extends CustomError {
|
||||
constructor (customMessage) {
|
||||
super();
|
||||
@@ -30,13 +47,42 @@ export class BadRequest extends CustomError {
|
||||
}
|
||||
}
|
||||
|
||||
// InternalError error with a 500 http error code
|
||||
// used when an unexpected, internal server error is thrown
|
||||
/**
|
||||
* @apiDefine NotFound
|
||||
* @apiError NotFound The requested resource was not found.
|
||||
*
|
||||
* @apiErrorExample Error-Response:
|
||||
* HTTP/1.1 404 Not Found
|
||||
* {
|
||||
* "error": "NotFound",
|
||||
* "message": "Not found."
|
||||
* }
|
||||
*/
|
||||
export class NotFound extends CustomError {
|
||||
constructor (customMessage) {
|
||||
super();
|
||||
this.name = this.constructor.name;
|
||||
this.httpCode = 404;
|
||||
this.message = customMessage || 'Not found.';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @apiDefine InternalServerError
|
||||
* @apiError InternalServerError An unexpected error occurred.
|
||||
*
|
||||
* @apiErrorExample Error-Response:
|
||||
* HTTP/1.1 500 Internal Server Error
|
||||
* {
|
||||
* "error": "InternalServerError",
|
||||
* "message": "An unexpected error occurred."
|
||||
* }
|
||||
*/
|
||||
export class InternalServerError extends CustomError {
|
||||
constructor (customMessage) {
|
||||
super();
|
||||
this.name = this.constructor.name;
|
||||
this.httpCode = 500;
|
||||
this.message = customMessage || 'Internal server error.';
|
||||
this.message = customMessage || 'An unexpected error occurred.';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ if (IS_PROD) {
|
||||
logger
|
||||
.add(winston.transports.Console, {
|
||||
colorize: true,
|
||||
prettyPrint: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
import express from 'express';
|
||||
import _ from 'lodash';
|
||||
|
||||
const CONTROLLERS_PATH = path.join(__dirname, '/../../controllers/api-v3/');
|
||||
let router = express.Router(); // eslint-disable-line new-cap
|
||||
|
||||
@@ -20,4 +21,4 @@ fs
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
export default router;
|
||||
|
||||
@@ -4,9 +4,16 @@ import analytics from './analytics';
|
||||
import errorHandler from './errorHandler';
|
||||
import bodyParser from 'body-parser';
|
||||
import routes from '../../libs/api-v3/setupRoutes';
|
||||
import notFoundHandler from './notFound';
|
||||
import nconf from 'nconf';
|
||||
import morgan from 'morgan';
|
||||
|
||||
const IS_PROD = nconf.get('IS_PROD');
|
||||
const DISABLE_LOGGING = nconf.get('DISABLE_REQUEST_LOGGING');
|
||||
|
||||
export default function attachMiddlewares (app) {
|
||||
// Parse query parameters and json bodies
|
||||
if (!IS_PROD && !DISABLE_LOGGING) app.use(morgan('dev'));
|
||||
|
||||
// TODO handle errors
|
||||
app.use(bodyParser.urlencoded({
|
||||
extended: true, // Uses 'qs' library as old connect middleware
|
||||
@@ -14,7 +21,8 @@ export default function attachMiddlewares (app) {
|
||||
app.use(bodyParser.json());
|
||||
app.use(analytics);
|
||||
|
||||
app.use(routes);
|
||||
app.use('/api/v3', routes);
|
||||
app.use(notFoundHandler);
|
||||
|
||||
// Error handler middleware, define as the last one
|
||||
app.use(errorHandler);
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import {
|
||||
NotFound,
|
||||
} from '../../libs/api-v3/errors';
|
||||
|
||||
export default function (req, res, next) {
|
||||
next(new NotFound());
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import express from 'express';
|
||||
import nconf from 'nconf';
|
||||
import path from 'path';
|
||||
|
||||
const IS_PROD = nconf.get('IS_PROD');
|
||||
const MAX_AGE = IS_PROD ? 31536000000 : 0;
|
||||
const PUBLIC_DIR = path.join(__dirname, '/../../../public');
|
||||
const BUILD_DIR = path.join(__dirname, '/../../../build');
|
||||
|
||||
export default function staticMiddleware (expressApp) {
|
||||
// TODO move all static files to a single location (one for public and one for build)
|
||||
expressApp.use(express.static(BUILD_DIR, { maxAge: MAX_AGE }));
|
||||
expressApp.use('/common/dist', express.static(`${PUBLIC_DIR}/../../common/dist`, { maxAge: MAX_AGE }));
|
||||
expressApp.use('/common/audio', express.static(`${PUBLIC_DIR}/../../common/audio`, { maxAge: MAX_AGE }));
|
||||
expressApp.use('/common/script/public', express.static(`${PUBLIC_DIR}/../../common/script/public`, { maxAge: MAX_AGE }));
|
||||
expressApp.use('/common/img', express.static(`${PUBLIC_DIR}/../../common/img`, { maxAge: MAX_AGE }));
|
||||
expressApp.use(express.static(PUBLIC_DIR));
|
||||
}
|
||||
+13
-8
@@ -14,6 +14,7 @@ import mongoose from 'mongoose';
|
||||
import Q from 'q';
|
||||
import domainMiddleware from './middlewares/api-v3/domain';
|
||||
import attachMiddlewares from './middlewares/api-v3/index';
|
||||
import staticMiddleware from './middlewares/api-v3/static';
|
||||
|
||||
// Setup translations
|
||||
// let i18n = require('./libs/api-v2/i18n');
|
||||
@@ -88,7 +89,7 @@ app.use(domainMiddleware(server, mongoose));
|
||||
// Matches all request except the ones going to /api/v3/**
|
||||
app.all(/^(?!\/api\/v3).+/i, oldApp);
|
||||
// Matches all requests going to /api/v3
|
||||
app.all('/api/v3', newApp);
|
||||
app.all('/api/*', newApp);
|
||||
|
||||
// Mount middlewares for the new app
|
||||
attachMiddlewares(newApp);
|
||||
@@ -144,18 +145,22 @@ oldApp.use('/api/v1', require('./routes/api-v1'));
|
||||
oldApp.use('/export', require('./routes/dataexport'));
|
||||
require('./routes/api-v2/swagger')(swagger, v2);
|
||||
|
||||
var maxAge = IS_PROD ? 31536000000 : 0;
|
||||
// Cache emojis without copying them to build, they are too many
|
||||
oldApp.use(express['static'](path.join(__dirname, "/../build"), { maxAge: maxAge }));
|
||||
oldApp.use('/common/dist', express['static'](publicDir + "/../../common/dist", { maxAge: maxAge }));
|
||||
oldApp.use('/common/audio', express['static'](publicDir + "/../../common/audio", { maxAge: maxAge }));
|
||||
oldApp.use('/common/script/public', express['static'](publicDir + "/../../common/script/public", { maxAge: maxAge }));
|
||||
oldApp.use('/common/img', express['static'](publicDir + "/../../common/img", { maxAge: maxAge }));
|
||||
oldApp.use(express['static'](publicDir));
|
||||
|
||||
oldApp.use(require('./middlewares/api-v2/errorHandler'));
|
||||
*
|
||||
let maxAge = IS_PROD ? 31536000000 : 0;
|
||||
|
||||
oldApp.use(express.static(path.join(__dirname, '/../build'), { maxAge }));
|
||||
oldApp.use('/common/dist', express.static(`${publicDir}/../../common/dist`, { maxAge }));
|
||||
oldApp.use('/common/audio', express.static(`${publicDir}/../../common/audio`, { maxAge }));
|
||||
oldApp.use('/common/script/public', express.static(`${publicDir}/../../common/script/public`, { maxAge }));
|
||||
oldApp.use('/common/img', express.static(`${publicDir}/../../common/img`, { maxAge }));
|
||||
oldApp.use(express.static(publicDir));
|
||||
*/
|
||||
|
||||
staticMiddleware(app);
|
||||
|
||||
server.on('request', app);
|
||||
server.listen(app.get('port'), () => {
|
||||
return logger.info(`Express server listening on port ${app.get('port')}`);
|
||||
|
||||
Reference in New Issue
Block a user