Compare commits
25 Commits
Author | SHA1 | Date | |
---|---|---|---|
506d42f39a | |||
3dfa72ddee | |||
bab2e90cbb | |||
da6b3ce963 | |||
b032be20d1 | |||
79c6a81a3a | |||
517f2b8157 | |||
a9fa790ae9 | |||
ce6f1fedde | |||
db86507ef9 | |||
6ad9df2db1 | |||
17105f66f4 | |||
b18479e02e | |||
0e9cd935b9 | |||
9524e55690 | |||
1752d700f5 | |||
6d289d6d78 | |||
0d6a715511 | |||
e5d72fd034 | |||
a0aeaef3a9 | |||
79b9fc1242 | |||
fb8275abca | |||
098aabfea5 | |||
f95f411a65 | |||
0416986896 |
7
CONTRIBUTING
Normal file
7
CONTRIBUTING
Normal 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).
|
32
README.md
32
README.md
@ -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.
|
||||
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
112
src/crud.js
112
src/crud.js
@ -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,
|
||||
};
|
||||
|
57
src/error.js
57
src/error.js
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
if (!global._babelPolyfill) {
|
||||
require('babel/polyfill');
|
||||
require('babel-polyfill');
|
||||
}
|
||||
|
||||
import crud, { associations } from './crud';
|
||||
|
Reference in New Issue
Block a user