25 Commits

Author SHA1 Message Date
506d42f39a 2.5.4 2016-09-05 12:30:15 -07:00
3dfa72ddee Merge pull request #21 from Getable/better-error
Fix a bug in error parsing
2016-09-05 12:25:14 -07:00
bab2e90cbb Feat (error) parse PG 42* errors 2016-09-04 17:28:16 -07:00
da6b3ce963 Feat (error) include hint on PG errors
Provides useful info!
2016-09-04 17:28:09 -07:00
b032be20d1 Fix (error) always reply with an error #oops
We had a case where reply would never be called. This could case a
server hang.
2016-09-04 17:28:01 -07:00
79c6a81a3a chore(version): bump version 2016-08-30 09:49:07 +04:30
517f2b8157 Merge pull request #17 from Getable/fix-babel-polyfill
Fixes for babel-polyfill
2016-08-30 09:48:20 +04:30
a9fa790ae9 Fix (deps): install babel-polyfill as optionalDep
We only require it if something else hasn't so this is a optionalDep.
2016-08-29 17:08:44 -07:00
ce6f1fedde Fix: bad merge in error.js 2016-08-28 09:57:23 -07:00
db86507ef9 Fix: correct babel-polyfill version
This installs the babel-polyfill for babel 6. #oops
2016-08-28 09:56:46 -07:00
6ad9df2db1 chore: bump version 2016-08-26 13:15:50 +04:30
17105f66f4 feat(errors): parse sequelize errors and use the relevant Boom error type 2016-08-26 13:15:15 +04:30
b18479e02e Merge pull request #16 from Getable/parse-sequelize-errors
Parse Sequelize errors
2016-08-26 13:11:29 +04:30
0e9cd935b9 Add: parse Sequelize errors
This adds intelligence around sequelize errors to parse out the correct
error status code and ensure it's always formatted as a Boom error.
2016-08-25 21:28:23 -07:00
9524e55690 chore(version): bump version 2016-08-04 10:38:18 +04:30
1752d700f5 Merge pull request #15 from Getable/better-errors
Add: Better errors
2016-08-04 10:37:29 +04:30
6d289d6d78 Add: Better errors
Now looks at the error that comes back from sequelize and uses boom to
format the error in a more friendly way. This should yield useful error
messages instead of generic 500s.
2016-08-03 14:42:20 -07:00
0d6a715511 chore(CONTRIBUTING): a contribution guide, git commit messages and code linting
@joeybaker ^
2016-07-22 23:29:54 +04:30
e5d72fd034 chore(version): bump version to 2.5.0, we have a new feature 2016-07-22 23:25:55 +04:30
a0aeaef3a9 Merge pull request #14 from Getable/fix-permissions
Change: permissions must always be an array
2016-07-22 23:25:26 +04:30
79b9fc1242 Change: permissions must always be an array
This allows us to set different configs per model. I should have thought
of this usecase when I first did permissions.
2016-07-22 11:50:08 -07:00
fb8275abca Merge pull request #12 from Getable/rm-extraneous-babel
Build: rm extraneous babel dep
2016-07-22 21:59:26 +04:30
098aabfea5 Merge pull request #11 from Getable/add-permissions
Add: permissions
2016-07-22 21:58:36 +04:30
f95f411a65 Add: permissions
It's now possible to limit the models rest routes are created for. This
is done via a `models` option that can be simple to complex. The readme
has been updated to reflect this.
2016-07-22 10:14:17 -07:00
0416986896 Build: rm extraneous babel dep #oops 2016-07-22 09:52:21 -07:00
6 changed files with 177 additions and 39 deletions

7
CONTRIBUTING Normal file
View File

@ -0,0 +1,7 @@
Commit Message
===============
Please follow [this convention](http://karma-runner.github.io/1.0/dev/git-commit-msg.html) for git commit message.
Lint
====
Please lint your code using `npm run lint` (also `npm run lint -- --fix` to auto-fix).

View File

@ -29,11 +29,41 @@ await register({
options: {
prefix: '/v1',
name: 'db', // the same name you used for configuring `hapi-sequelize` (options.name)
defaultConfig: { ... } // passed as `config` to all routes created
defaultConfig: { ... }, // passed as `config` to all routes created
// You can specify which models must have routes defined for using the
// `models` property. If you omit this property, all models will have
// models defined for them. e.g.
models: ['cat', 'dog'] // only the cat and dog models will have routes created
// or
models: [
// possible methods: list, get, scope, create, destroy, destroyAll, destroyScope, update
// the cat model only has get and list methods enabled
{model: 'cat', methods: ['get', 'list']},
// the dog model has all methods enabled
{model: 'dog'},
// the cow model also has all methods enabled
'cow',
// the bat model as a custom config for the list method, but uses the default config for create.
// `config` if provided, overrides the default config
{model: 'bat', methods: ['list'], config: { ... }},
{model: 'bat', methods: ['create']}
]
}
});
```
### Methods
* list: get all rows in a table
* get: get a single row
* scope: reference a [sequelize scope](http://docs.sequelizejs.com/en/latest/api/model/#scopeoptions-model)
* create: create a new row
* destroy: delete a row
* destroyAll: delete all models in the table
* destroyScope: use a [sequelize scope](http://docs.sequelizejs.com/en/latest/api/model/#scopeoptions-model) to find rows, then delete them
* update: update a row
Please note that you should register `hapi-sequelize-crud` after defining your
associations.

View File

@ -1,6 +1,6 @@
{
"name": "hapi-sequelize-crud",
"version": "2.4.0",
"version": "2.5.4",
"description": "Hapi plugin that automatically generates RESTful API for CRUD",
"main": "build/index.js",
"config": {
@ -35,9 +35,11 @@
"scripty": "^1.6.0"
},
"dependencies": {
"babel": "5.8.3",
"boom": "^3.2.2",
"joi": "7.2.1",
"lodash": "4.0.0"
},
"optionalDependencies": {
"babel-polyfill": "^6.13.0"
}
}

View File

@ -3,25 +3,70 @@ import error from './error';
import _ from 'lodash';
import { parseInclude, parseWhere } from './utils';
import { notFound } from 'boom';
import * as associations from './associations/index';
let prefix;
let defaultConfig;
export default (server, model, options) => {
prefix = options.prefix;
defaultConfig = options.defaultConfig;
list(server, model);
get(server, model);
scope(server, model);
create(server, model);
destroy(server, model);
destroyAll(server, model);
destroyScope(server, model);
update(server, model);
const createAll = ({ server, model, prefix, config }) => {
Object.keys(methods).forEach((method) => {
methods[method]({ server, model, prefix, config });
});
};
export const list = (server, model) => {
export { associations };
/*
The `models` option, becomes `permissions`, and can look like:
```
models: ['cat', 'dog']
```
or
```
models: {
cat: ['list', 'get']
, dog: true // all
}
```
*/
export default (server, model, { prefix, defaultConfig: config, models: permissions }) => {
const modelName = model._singular;
if (!permissions) {
createAll({ server, model, prefix, config });
} else if (!Array.isArray(permissions)) {
throw new Error('hapi-sequelize-crud: `models` property must be an array');
} else if (permissions.includes(modelName)) {
createAll({ server, model, prefix, config });
} else {
const permissionOptions = permissions.filter((permission) => {
return permission.model === modelName;
});
permissionOptions.forEach((permissionOption) => {
if (_.isPlainObject(permissionOption)) {
const permissionConfig = permissionOption.config || config;
if (permissionOption.methods) {
permissionOption.methods.forEach((method) => {
methods[method]({
server,
model,
prefix,
config: permissionConfig,
});
});
} else {
createAll({ server, model, prefix, config: permissionConfig });
}
}
});
}
};
export const list = ({ server, model, prefix, config }) => {
server.route({
method: 'GET',
path: `${prefix}/${model._plural}`,
@ -40,11 +85,11 @@ export const list = (server, model) => {
reply(list);
},
config: defaultConfig,
config,
});
};
export const get = (server, model) => {
export const get = ({ server, model, prefix, config }) => {
server.route({
method: 'GET',
path: `${prefix}/${model._singular}/{id?}`,
@ -68,11 +113,11 @@ export const get = (server, model) => {
id: joi.any(),
}),
},
}, defaultConfig),
}, config),
});
};
export const scope = (server, model) => {
export const scope = ({ server, model, prefix, config }) => {
const scopes = Object.keys(model.options.scopes);
server.route({
@ -94,11 +139,11 @@ export const scope = (server, model) => {
scope: joi.string().valid(...scopes),
}),
},
}, defaultConfig),
}, config),
});
};
export const create = (server, model) => {
export const create = ({ server, model, prefix, config }) => {
server.route({
method: 'POST',
path: `${prefix}/${model._singular}`,
@ -110,11 +155,11 @@ export const create = (server, model) => {
reply(instance);
},
config: defaultConfig,
config,
});
};
export const destroy = (server, model) => {
export const destroy = ({ server, model, prefix, config }) => {
server.route({
method: 'DELETE',
path: `${prefix}/${model._singular}/{id?}`,
@ -131,11 +176,11 @@ export const destroy = (server, model) => {
reply(list.length === 1 ? list[0] : list);
},
config: defaultConfig,
config,
});
};
export const destroyAll = (server, model) => {
export const destroyAll = ({ server, model, prefix, config }) => {
server.route({
method: 'DELETE',
path: `${prefix}/${model._plural}`,
@ -151,11 +196,11 @@ export const destroyAll = (server, model) => {
reply(list.length === 1 ? list[0] : list);
},
config: defaultConfig,
config,
});
};
export const destroyScope = (server, model) => {
export const destroyScope = ({ server, model, prefix, config }) => {
const scopes = Object.keys(model.options.scopes);
server.route({
@ -179,11 +224,11 @@ export const destroyScope = (server, model) => {
scope: joi.string().valid(...scopes),
}),
},
}, defaultConfig),
}, config),
});
};
export const update = (server, model) => {
export const update = ({ server, model, prefix, config }) => {
server.route({
method: 'PUT',
path: `${prefix}/${model._singular}/{id}`,
@ -208,9 +253,10 @@ export const update = (server, model) => {
validate: {
payload: joi.object().required(),
},
}, defaultConfig),
}, config),
});
};
import * as associations from './associations/index';
export { associations };
const methods = {
list, get, scope, create, destroy, destroyAll, destroyScope, update,
};

View File

@ -1,3 +1,5 @@
import Boom from 'boom';
export default (target, key, descriptor) => {
const fn = descriptor.value;
@ -5,8 +7,59 @@ export default (target, key, descriptor) => {
try {
await fn(request, reply);
} catch (e) {
console.error(e);
reply(e);
if (e.original) {
const { code, detail, hint } = e.original;
let error;
// pg error codes https://www.postgresql.org/docs/9.5/static/errcodes-appendix.html
if (code && (code.startsWith('22') || code.startsWith('23'))) {
error = Boom.wrap(e, 406);
} else if (code && (code.startsWith('42'))) {
error = Boom.wrap(e, 422);
// TODO: we could get better at parse postgres error codes
} else {
// use a 502 error code since the issue is upstream with postgres, not
// this server
error = Boom.wrap(e, 502);
}
// 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) {
const { message } = e;
let err;
if (e.name === 'SequelizeValidationError')
err = Boom.badData(message);
else if (e.name === 'SequelizeConnectionTimedOutError')
err = Boom.gatewayTimeout(message);
else if (e.name === 'SequelizeHostNotReachableError')
err = Boom.serverUnavailable(message);
else if (e.name === 'SequelizeUniqueConstraintError')
err = Boom.conflict(message);
else if (e.name === 'SequelizeForeignKeyConstraintError')
err = Boom.expectationFailed(message);
else if (e.name === 'SequelizeExclusionConstraintError')
err = Boom.expectationFailed(message);
else if (e.name === 'SequelizeConnectionError')
err = Boom.badGateway(message);
else err = Boom.badImplementation(message);
reply(err);
} else {
reply(e);
}
}
};

View File

@ -1,5 +1,5 @@
if (!global._babelPolyfill) {
require('babel/polyfill');
require('babel-polyfill');
}
import crud, { associations } from './crud';