Merge branch 'master' of github.com:mdibaiee/hapi-sequelize-crud
This commit is contained in:
commit
3e53ba8d2c
@ -1,3 +1,9 @@
|
|||||||
{
|
{
|
||||||
"extends": "pichak"
|
"plugins": [
|
||||||
|
"ava"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"pichak",
|
||||||
|
"plugin:ava/recommended"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
9
circle.yml
Normal file
9
circle.yml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
machine:
|
||||||
|
node:
|
||||||
|
version: 6.5.0
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
pre:
|
||||||
|
- npm prune
|
||||||
|
post:
|
||||||
|
- mkdir -p $CIRCLE_TEST_REPORTS/ava
|
16
package.json
16
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hapi-sequelize-crud",
|
"name": "hapi-sequelize-crud",
|
||||||
"version": "2.5.3",
|
"version": "2.5.4",
|
||||||
"description": "Hapi plugin that automatically generates RESTful API for CRUD",
|
"description": "Hapi plugin that automatically generates RESTful API for CRUD",
|
||||||
"main": "build/index.js",
|
"main": "build/index.js",
|
||||||
"config": {
|
"config": {
|
||||||
@ -9,8 +9,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint src test",
|
"lint": "eslint src",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "ava --require babel-register --source='*.test.js' --tap=${CI-false} | $(if [ -z ${CI:-} ]; then echo 'tail'; else tap-xunit > $CIRCLE_TEST_REPORTS/ava/ava.xml; fi;)",
|
||||||
|
"tdd": "ava --require babel-register --source='*.test.js' --watch",
|
||||||
"build": "scripty",
|
"build": "scripty",
|
||||||
"watch": "scripty"
|
"watch": "scripty"
|
||||||
},
|
},
|
||||||
@ -23,16 +24,21 @@
|
|||||||
"author": "Mahdi Dibaiee <mdibaiee@aol.com> (http://dibaiee.ir/)",
|
"author": "Mahdi Dibaiee <mdibaiee@aol.com> (http://dibaiee.ir/)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"ava": "^0.16.0",
|
||||||
"babel-cli": "^6.10.1",
|
"babel-cli": "^6.10.1",
|
||||||
"babel-plugin-add-module-exports": "^0.2.1",
|
"babel-plugin-add-module-exports": "^0.2.1",
|
||||||
"babel-plugin-closure-elimination": "^1.0.6",
|
"babel-plugin-closure-elimination": "^1.0.6",
|
||||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||||
"babel-plugin-transform-es2015-modules-commonjs": "^6.10.3",
|
"babel-plugin-transform-es2015-modules-commonjs": "^6.10.3",
|
||||||
"babel-preset-stage-1": "^6.5.0",
|
"babel-preset-stage-1": "^6.5.0",
|
||||||
"eslint": "2.10.2",
|
"eslint": "^3.4.0",
|
||||||
"eslint-config-pichak": "1.1.0",
|
"eslint-config-pichak": "1.1.0",
|
||||||
|
"eslint-plugin-ava": "^3.0.0",
|
||||||
"ghooks": "1.0.3",
|
"ghooks": "1.0.3",
|
||||||
"scripty": "^1.6.0"
|
"scripty": "^1.6.0",
|
||||||
|
"sinon": "^1.17.5",
|
||||||
|
"sinon-bluebird": "^3.0.2",
|
||||||
|
"tap-xunit": "^1.4.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"boom": "^3.2.2",
|
"boom": "^3.2.2",
|
||||||
|
33
src/crud.js
33
src/crud.js
@ -1,4 +1,5 @@
|
|||||||
import joi from 'joi';
|
import joi from 'joi';
|
||||||
|
import path from 'path';
|
||||||
import error from './error';
|
import error from './error';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { parseInclude, parseWhere } from './utils';
|
import { parseInclude, parseWhere } from './utils';
|
||||||
@ -66,10 +67,10 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const list = ({ server, model, prefix, config }) => {
|
export const list = ({ server, model, prefix = '/', config }) => {
|
||||||
server.route({
|
server.route({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: `${prefix}/${model._plural}`,
|
path: path.join(prefix, model._plural),
|
||||||
|
|
||||||
@error
|
@error
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
@ -89,10 +90,10 @@ export const list = ({ server, model, prefix, config }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const get = ({ server, model, prefix, config }) => {
|
export const get = ({ server, model, prefix = '/', config }) => {
|
||||||
server.route({
|
server.route({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: `${prefix}/${model._singular}/{id?}`,
|
path: path.join(prefix, model._singular, '{id?}'),
|
||||||
|
|
||||||
@error
|
@error
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
@ -117,12 +118,12 @@ export const get = ({ server, model, prefix, config }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const scope = ({ server, model, prefix, config }) => {
|
export const scope = ({ server, model, prefix = '/', config }) => {
|
||||||
const scopes = Object.keys(model.options.scopes);
|
const scopes = Object.keys(model.options.scopes);
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: `${prefix}/${model._plural}/{scope}`,
|
path: path.join(prefix, model._plural, '{scope}'),
|
||||||
|
|
||||||
@error
|
@error
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
@ -143,10 +144,10 @@ export const scope = ({ server, model, prefix, config }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const create = ({ server, model, prefix, config }) => {
|
export const create = ({ server, model, prefix = '/', config }) => {
|
||||||
server.route({
|
server.route({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: `${prefix}/${model._singular}`,
|
path: path.join(prefix, model._singular),
|
||||||
|
|
||||||
@error
|
@error
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
@ -159,10 +160,10 @@ export const create = ({ server, model, prefix, config }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const destroy = ({ server, model, prefix, config }) => {
|
export const destroy = ({ server, model, prefix = '/', config }) => {
|
||||||
server.route({
|
server.route({
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
path: `${prefix}/${model._singular}/{id?}`,
|
path: path.join(prefix, model._singular, '{id?}'),
|
||||||
|
|
||||||
@error
|
@error
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
@ -180,10 +181,10 @@ export const destroy = ({ server, model, prefix, config }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const destroyAll = ({ server, model, prefix, config }) => {
|
export const destroyAll = ({ server, model, prefix = '/', config }) => {
|
||||||
server.route({
|
server.route({
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
path: `${prefix}/${model._plural}`,
|
path: path.join(prefix, model._plural),
|
||||||
|
|
||||||
@error
|
@error
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
@ -200,12 +201,12 @@ export const destroyAll = ({ server, model, prefix, config }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const destroyScope = ({ server, model, prefix, config }) => {
|
export const destroyScope = ({ server, model, prefix = '/', config }) => {
|
||||||
const scopes = Object.keys(model.options.scopes);
|
const scopes = Object.keys(model.options.scopes);
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
path: `${prefix}/${model._plural}/{scope}`,
|
path: path.join(prefix, model._plural, '{scope}'),
|
||||||
|
|
||||||
@error
|
@error
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
@ -228,10 +229,10 @@ export const destroyScope = ({ server, model, prefix, config }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const update = ({ server, model, prefix, config }) => {
|
export const update = ({ server, model, prefix = '/', config }) => {
|
||||||
server.route({
|
server.route({
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
path: `${prefix}/${model._singular}/{id}`,
|
path: path.join(prefix, model._singular, '{id}'),
|
||||||
|
|
||||||
@error
|
@error
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
|
146
src/crud.test.js
Normal file
146
src/crud.test.js
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import test from 'ava';
|
||||||
|
import { list } from './crud.js';
|
||||||
|
import { stub } from 'sinon';
|
||||||
|
import 'sinon-bluebird';
|
||||||
|
|
||||||
|
const METHODS = {
|
||||||
|
GET: 'GET',
|
||||||
|
};
|
||||||
|
|
||||||
|
test.beforeEach('setup server', (t) => {
|
||||||
|
t.context.server = {
|
||||||
|
route: stub(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
test.beforeEach('setup model', (t) => {
|
||||||
|
t.context.model = {
|
||||||
|
findAll: stub(),
|
||||||
|
_plural: 'models',
|
||||||
|
_singular: 'model',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
test.beforeEach('setup request stub', (t) => {
|
||||||
|
t.context.request = {
|
||||||
|
query: {},
|
||||||
|
payload: {},
|
||||||
|
models: [t.context.model],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
test.beforeEach('setup reply stub', (t) => {
|
||||||
|
t.context.reply = stub();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('crud#list without prefix', (t) => {
|
||||||
|
const { server, model } = t.context;
|
||||||
|
|
||||||
|
list({ server, model });
|
||||||
|
const { path } = server.route.args[0][0];
|
||||||
|
|
||||||
|
t.falsy(
|
||||||
|
path.includes('undefined'),
|
||||||
|
'correctly sets the path without a prefix defined',
|
||||||
|
);
|
||||||
|
|
||||||
|
t.is(
|
||||||
|
path,
|
||||||
|
`/${model._plural}`,
|
||||||
|
'the path sets to the plural model'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('crud#list with prefix', (t) => {
|
||||||
|
const { server, model } = t.context;
|
||||||
|
const prefix = '/v1';
|
||||||
|
|
||||||
|
list({ server, model, prefix });
|
||||||
|
const { path } = server.route.args[0][0];
|
||||||
|
|
||||||
|
t.is(
|
||||||
|
path,
|
||||||
|
`${prefix}/${model._plural}`,
|
||||||
|
'the path sets to the plural model with the prefix'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('crud#list method', (t) => {
|
||||||
|
const { server, model } = t.context;
|
||||||
|
|
||||||
|
list({ server, model });
|
||||||
|
const { method } = server.route.args[0][0];
|
||||||
|
|
||||||
|
t.is(
|
||||||
|
method,
|
||||||
|
METHODS.GET,
|
||||||
|
`sets the method to ${METHODS.GET}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('crud#list config', (t) => {
|
||||||
|
const { server, model } = t.context;
|
||||||
|
const userConfig = {};
|
||||||
|
|
||||||
|
list({ server, model, config: userConfig });
|
||||||
|
const { config } = server.route.args[0][0];
|
||||||
|
|
||||||
|
t.is(
|
||||||
|
config,
|
||||||
|
userConfig,
|
||||||
|
'sets the user config'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('crud#list handler', async (t) => {
|
||||||
|
const { server, model, request, reply } = t.context;
|
||||||
|
const allModels = [{ id: 1 }, { id: 2 }];
|
||||||
|
|
||||||
|
list({ server, model });
|
||||||
|
const { handler } = server.route.args[0][0];
|
||||||
|
model.findAll.resolves(allModels);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await handler(request, reply);
|
||||||
|
} catch (e) {
|
||||||
|
t.ifError(e, 'does not error while handling');
|
||||||
|
} finally {
|
||||||
|
t.pass('does not error while handling');
|
||||||
|
}
|
||||||
|
|
||||||
|
t.truthy(
|
||||||
|
reply.calledOnce
|
||||||
|
, 'calls reply only once'
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = reply.args[0][0];
|
||||||
|
|
||||||
|
t.is(
|
||||||
|
response,
|
||||||
|
allModels,
|
||||||
|
'responds with the list of models'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('crud#list handler if parseInclude errors', async (t) => {
|
||||||
|
const { server, model, request, reply } = t.context;
|
||||||
|
// we _want_ the error
|
||||||
|
delete request.models;
|
||||||
|
|
||||||
|
list({ server, model });
|
||||||
|
const { handler } = server.route.args[0][0];
|
||||||
|
|
||||||
|
await handler(request, reply);
|
||||||
|
|
||||||
|
t.truthy(
|
||||||
|
reply.calledOnce
|
||||||
|
, 'calls reply only once'
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = reply.args[0][0];
|
||||||
|
|
||||||
|
t.truthy(
|
||||||
|
response.isBoom,
|
||||||
|
'responds with a Boom error'
|
||||||
|
);
|
||||||
|
});
|
34
src/error.js
34
src/error.js
@ -8,20 +8,34 @@ export default (target, key, descriptor) => {
|
|||||||
await fn(request, reply);
|
await fn(request, reply);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.original) {
|
if (e.original) {
|
||||||
const { code, detail } = e.original;
|
const { code, detail, hint } = e.original;
|
||||||
|
let error;
|
||||||
|
|
||||||
// pg error codes https://www.postgresql.org/docs/9.5/static/errcodes-appendix.html
|
// pg error codes https://www.postgresql.org/docs/9.5/static/errcodes-appendix.html
|
||||||
if (code && (code.startsWith('22') || code.startsWith('23'))) {
|
if (code && (code.startsWith('22') || code.startsWith('23'))) {
|
||||||
const error = Boom.wrap(e, 406);
|
error = Boom.wrap(e, 406);
|
||||||
|
} else if (code && (code.startsWith('42'))) {
|
||||||
// detail tends to be more specific information. So, if we have it, use.
|
error = Boom.wrap(e, 422);
|
||||||
if (detail) {
|
// TODO: we could get better at parse postgres error codes
|
||||||
error.message += `: ${detail}`;
|
} else {
|
||||||
error.reformat();
|
// use a 502 error code since the issue is upstream with postgres, not
|
||||||
}
|
// this server
|
||||||
|
error = Boom.wrap(e, 502);
|
||||||
reply(error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// detail tends to be more specific information. So, if we have it, use.
|
||||||
|
if (detail) {
|
||||||
|
error.message += `: ${detail}`;
|
||||||
|
error.reformat();
|
||||||
|
}
|
||||||
|
|
||||||
|
// hint might provide useful information about how to fix the problem
|
||||||
|
if (hint) {
|
||||||
|
error.message += ` Hint: ${hint}`;
|
||||||
|
error.reformat();
|
||||||
|
}
|
||||||
|
|
||||||
|
reply(error);
|
||||||
} else if (!e.isBoom) {
|
} else if (!e.isBoom) {
|
||||||
const { message } = e;
|
const { message } = e;
|
||||||
let err;
|
let err;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { omit, identity } from 'lodash';
|
import { omit, identity } from 'lodash';
|
||||||
|
import { notImplemented } from 'boom';
|
||||||
|
|
||||||
export const parseInclude = request => {
|
export const parseInclude = request => {
|
||||||
const include = Array.isArray(request.query.include) ? request.query.include
|
const include = Array.isArray(request.query.include) ? request.query.include
|
||||||
@ -8,7 +9,7 @@ export const parseInclude = request => {
|
|||||||
const noRequestModels = !request.models;
|
const noRequestModels = !request.models;
|
||||||
|
|
||||||
if (noGetDb && noRequestModels) {
|
if (noGetDb && noRequestModels) {
|
||||||
return new Error('`request.getDb` or `request.models` are not defined.'
|
return notImplemented('`request.getDb` or `request.models` are not defined.'
|
||||||
+ 'Be sure to load hapi-sequelize before hapi-sequelize-crud.');
|
+ 'Be sure to load hapi-sequelize before hapi-sequelize-crud.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user