Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
a855665777 | |||
034287672c | |||
e8c0e61c6b | |||
a64a55af0d |
@ -1,9 +1,3 @@
|
|||||||
{
|
{
|
||||||
"plugins": [
|
"extends": "pichak"
|
||||||
"ava"
|
|
||||||
],
|
|
||||||
"extends": [
|
|
||||||
"pichak",
|
|
||||||
"plugin:ava/recommended"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
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).
|
|
94
README.md
94
README.md
@ -1,4 +1,4 @@
|
|||||||
hapi-sequelize-crud [](https://circleci.com/gh/mdibaiee/hapi-sequelize-crud)
|
hapi-sequelize-crud
|
||||||
===================
|
===================
|
||||||
|
|
||||||
Automatically generate a RESTful API for your models and associations
|
Automatically generate a RESTful API for your models and associations
|
||||||
@ -11,9 +11,6 @@ npm install -S hapi-sequelize-crud
|
|||||||
|
|
||||||
##Configure
|
##Configure
|
||||||
|
|
||||||
Please note that you should register `hapi-sequelize-crud` after defining your
|
|
||||||
associations.
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// First, register hapi-sequelize
|
// First, register hapi-sequelize
|
||||||
await register({
|
await register({
|
||||||
@ -33,84 +30,36 @@ await register({
|
|||||||
prefix: '/v1',
|
prefix: '/v1',
|
||||||
name: 'db', // the same name you used for configuring `hapi-sequelize` (options.name)
|
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
|
models: ['cat', 'dog'] // only the cat and dog models will have routes created
|
||||||
|
|
||||||
// or
|
// or
|
||||||
models: [
|
models: {
|
||||||
// possible methods: list, get, scope, create, destroy, destroyAll, destroyScope, update
|
// possible methods: list, get, scope, create, destroy, destroyAll, destroyScope, update
|
||||||
// the cat model only has get and list methods enabled
|
cat: ['get', 'list'], // the cat model only has get and list methods enabled
|
||||||
{model: 'cat', methods: ['get', 'list']},
|
dog: true, // the dog model has all methods enabled
|
||||||
// the dog model has all methods enabled
|
bat: {
|
||||||
{model: 'dog'},
|
methods: ['list'],
|
||||||
// the cow model also has all methods enabled
|
config: { ... } // if provided, overrides the default config
|
||||||
'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
|
### Methods
|
||||||
* **list**: get all rows in a table
|
* list: get all rows in a table
|
||||||
* **get**: get a single row
|
* get: get a single row
|
||||||
* **scope**: reference a [sequelize scope](http://docs.sequelizejs.com/en/latest/api/model/#scopeoptions-model)
|
* scope: reference a [sequelize scope](http://docs.sequelizejs.com/en/latest/api/model/#scopeoptions-model)
|
||||||
* **create**: create a new row
|
* create: create a new row
|
||||||
* **destroy**: delete a row
|
* destroy: delete a row
|
||||||
* **destroyAll**: delete all models in the table
|
* 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
|
* 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
|
* update: update a row
|
||||||
|
|
||||||
## `where` queries
|
|
||||||
It's easy to restrict your requests using Sequelize's `where` query option. Just pass a query parameter.
|
|
||||||
|
|
||||||
```js
|
Please note that you should register `hapi-sequelize-crud` after defining your
|
||||||
// returns only teams that have a `city` property of "windsor"
|
associations.
|
||||||
// GET /team?city=windsor
|
|
||||||
|
|
||||||
// results in the Sequelize query:
|
##What do I get
|
||||||
Team.findOne({ where: { city: 'windsor' }})
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also do more complex queries by setting the value of a key to JSON.
|
|
||||||
|
|
||||||
```js
|
|
||||||
// returns only teams that have a `address.city` property of "windsor"
|
|
||||||
// GET /team?city={"address": "windsor"}
|
|
||||||
// or
|
|
||||||
// GET /team?city[address]=windsor
|
|
||||||
|
|
||||||
// results in the Sequelize query:
|
|
||||||
Team.findOne({ where: { address: { city: 'windsor' }}})
|
|
||||||
```
|
|
||||||
|
|
||||||
## `include` queries
|
|
||||||
Getting related models is easy, just use a query parameter `include`.
|
|
||||||
|
|
||||||
```js
|
|
||||||
// returns all teams with their related City model
|
|
||||||
// GET /teams?include=City
|
|
||||||
|
|
||||||
// results in a Sequelize query:
|
|
||||||
Team.findAll({include: City})
|
|
||||||
```
|
|
||||||
|
|
||||||
If you want to get multiple related models, just pass multiple `include` parameters.
|
|
||||||
```js
|
|
||||||
// returns all teams with their related City and Uniform models
|
|
||||||
// GET /teams?include=City&include=Uniform
|
|
||||||
|
|
||||||
// results in a Sequelize query:
|
|
||||||
Team.findAll({include: [City, Uniform]})
|
|
||||||
```
|
|
||||||
|
|
||||||
## Full list of methods
|
|
||||||
|
|
||||||
Let's say you have a `many-to-many` association like this:
|
Let's say you have a `many-to-many` association like this:
|
||||||
|
|
||||||
@ -125,9 +74,8 @@ You get these:
|
|||||||
# get an array of records
|
# get an array of records
|
||||||
GET /team/{id}/roles
|
GET /team/{id}/roles
|
||||||
GET /role/{id}/teams
|
GET /role/{id}/teams
|
||||||
# might also append `where` query parameters to search for
|
# might also append query parameters to search for
|
||||||
GET /role/{id}/teams?members=5
|
GET /role/{id}/teams?members=5
|
||||||
GET /role/{id}/teams?city=healdsburg
|
|
||||||
|
|
||||||
# you might also use scopes
|
# you might also use scopes
|
||||||
GET /teams/{scope}/roles/{scope}
|
GET /teams/{scope}/roles/{scope}
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
machine:
|
|
||||||
node:
|
|
||||||
version: 6.5.0
|
|
||||||
|
|
||||||
dependencies:
|
|
||||||
pre:
|
|
||||||
- npm prune
|
|
||||||
post:
|
|
||||||
- mkdir -p $CIRCLE_TEST_REPORTS/ava
|
|
40
package.json
40
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hapi-sequelize-crud",
|
"name": "@getable/hapi-sequelize-crud",
|
||||||
"version": "2.6.0",
|
"version": "2.3.0",
|
||||||
"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,14 +9,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint src",
|
"lint": "eslint src test",
|
||||||
"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;)",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"tdd": "ava --require babel-register --source='*.test.js' --watch",
|
|
||||||
"build": "scripty",
|
"build": "scripty",
|
||||||
"watch": "scripty"
|
"watch": "scripty"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"git": "https://github.com/mdibaiee/hapi-sequelize-crud"
|
"git": "https://github.com/Getable/hapi-sequelize-crud"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"build"
|
"build"
|
||||||
@ -24,28 +23,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.14.0",
|
|
||||||
"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.14.0",
|
"babel-plugin-transform-es2015-modules-commonjs": "^6.10.3",
|
||||||
"babel-preset-stage-1": "^6.13.0",
|
"babel-preset-stage-1": "^6.5.0",
|
||||||
"eslint": "^3.4.0",
|
"eslint": "2.10.2",
|
||||||
"eslint-config-pichak": "^1.1.2",
|
"eslint-config-pichak": "1.1.0",
|
||||||
"eslint-plugin-ava": "^3.0.0",
|
"ghooks": "1.0.3",
|
||||||
"ghooks": "^1.3.2",
|
"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": "^4.0.0",
|
"babel": "5.8.3",
|
||||||
"joi": "^9.0.4",
|
"boom": "^3.2.2",
|
||||||
"lodash": "^4.15.0"
|
"joi": "7.2.1",
|
||||||
},
|
"lodash": "4.0.0"
|
||||||
"optionalDependencies": {
|
|
||||||
"babel-polyfill": "^6.13.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
186
src/crud.js
186
src/crud.js
@ -1,37 +1,15 @@
|
|||||||
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';
|
||||||
import { notFound } from 'boom';
|
import { notFound } from 'boom';
|
||||||
import * as associations from './associations/index';
|
|
||||||
import getConfigForMethod from './get-config-for-method.js';
|
|
||||||
|
|
||||||
const createAll = ({
|
const createAll = ({server, model, prefix, config}) => {
|
||||||
server,
|
|
||||||
model,
|
|
||||||
prefix,
|
|
||||||
config,
|
|
||||||
attributeValidation,
|
|
||||||
associationValidation,
|
|
||||||
}) => {
|
|
||||||
Object.keys(methods).forEach((method) => {
|
Object.keys(methods).forEach((method) => {
|
||||||
methods[method]({
|
methods[method]({server, model, prefix, config});
|
||||||
server,
|
|
||||||
model,
|
|
||||||
prefix,
|
|
||||||
config: getConfigForMethod({
|
|
||||||
method,
|
|
||||||
attributeValidation,
|
|
||||||
associationValidation,
|
|
||||||
config,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export { associations };
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
The `models` option, becomes `permissions`, and can look like:
|
The `models` option, becomes `permissions`, and can look like:
|
||||||
|
|
||||||
@ -50,87 +28,45 @@ models: {
|
|||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default (server, model, { prefix, defaultConfig: config, models: permissions }) => {
|
export default (server, model, {prefix, defaultConfig: config, models: permissions}) => {
|
||||||
const modelName = model._singular;
|
const modelName = model._singular;
|
||||||
const modelAttributes = Object.keys(model.attributes);
|
|
||||||
const modelAssociations = Object.keys(model.associations);
|
|
||||||
|
|
||||||
const attributeValidation = modelAttributes.reduce((params, attribute) => {
|
|
||||||
params[attribute] = joi.any();
|
|
||||||
return params;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const associationValidation = {
|
|
||||||
include: joi.array().items(joi.string().valid(...modelAssociations)),
|
|
||||||
};
|
|
||||||
|
|
||||||
// if we don't have any permissions set, just create all the methods
|
|
||||||
if (!permissions) {
|
if (!permissions) {
|
||||||
createAll({
|
createAll({server, model, prefix, config});
|
||||||
server,
|
}
|
||||||
model,
|
else if (Array.isArray(permissions) && permissions.includes(modelName)) {
|
||||||
prefix,
|
createAll({server, model, prefix, config});
|
||||||
config,
|
}
|
||||||
attributeValidation,
|
else if (_.isPlainObject(permissions)) {
|
||||||
associationValidation,
|
const permittedModels = Object.keys(permissions);
|
||||||
});
|
|
||||||
// if permissions are set, but we can't parse them, throw an error
|
|
||||||
} else if (!Array.isArray(permissions)) {
|
|
||||||
throw new Error('hapi-sequelize-crud: `models` property must be an array');
|
|
||||||
// if permissions are set, but the only thing we've got is a model name, there
|
|
||||||
// are no permissions to be set, so just create all methods and move on
|
|
||||||
} else if (permissions.includes(modelName)) {
|
|
||||||
createAll({
|
|
||||||
server,
|
|
||||||
model,
|
|
||||||
prefix,
|
|
||||||
config,
|
|
||||||
attributeValidation,
|
|
||||||
associationValidation,
|
|
||||||
});
|
|
||||||
// if we've gotten here, we have complex permissions and need to set them
|
|
||||||
} else {
|
|
||||||
const permissionOptions = permissions.filter((permission) => {
|
|
||||||
return permission.model === modelName;
|
|
||||||
});
|
|
||||||
|
|
||||||
permissionOptions.forEach((permissionOption) => {
|
if (permissions[modelName] === true) {
|
||||||
if (_.isPlainObject(permissionOption)) {
|
createAll({server, model, prefix, config});
|
||||||
const permissionConfig = permissionOption.config || config;
|
}
|
||||||
|
else if (permittedModels.includes(modelName)) {
|
||||||
if (permissionOption.methods) {
|
if (Array.isArray(permissions[modelName])) {
|
||||||
permissionOption.methods.forEach((method) => {
|
permissions[modelName].forEach((method) => {
|
||||||
|
methods[method]({server, model, prefix, config});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (_.isPlainObject(permissions[modelName])) {
|
||||||
|
permissions[modelName].methods.forEach((method) => {
|
||||||
methods[method]({
|
methods[method]({
|
||||||
server,
|
server,
|
||||||
model,
|
model,
|
||||||
prefix,
|
prefix,
|
||||||
config: getConfigForMethod({
|
config: permissions[modelName].config || config,
|
||||||
method,
|
|
||||||
attributeValidation,
|
|
||||||
associationValidation,
|
|
||||||
config: permissionConfig,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
createAll({
|
|
||||||
server,
|
|
||||||
model,
|
|
||||||
prefix,
|
|
||||||
attributeValidation,
|
|
||||||
associationValidation,
|
|
||||||
config: permissionConfig,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const list = ({ server, model, prefix = '/', config }) => {
|
export const list = ({server, model, prefix, config}) => {
|
||||||
server.route({
|
server.route({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: path.join(prefix, model._plural),
|
path: `${prefix}/${model._plural}`,
|
||||||
|
|
||||||
@error
|
@error
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
@ -150,10 +86,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: path.join(prefix, model._singular, '{id?}'),
|
path: `${prefix}/${model._singular}/{id?}`,
|
||||||
|
|
||||||
@error
|
@error
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
@ -162,56 +98,52 @@ export const get = ({ server, model, prefix = '/', config }) => {
|
|||||||
const { id } = request.params;
|
const { id } = request.params;
|
||||||
if (id) where[model.primaryKeyField] = id;
|
if (id) where[model.primaryKeyField] = id;
|
||||||
|
|
||||||
if (include instanceof Error) return void reply(include);
|
|
||||||
|
|
||||||
const instance = await model.findOne({ where, include });
|
const instance = await model.findOne({ where, include });
|
||||||
|
|
||||||
if (!instance) return void reply(notFound(`${id} not found.`));
|
if (!instance) return void reply(notFound(`${id} not found.`));
|
||||||
|
|
||||||
reply(instance);
|
reply(instance);
|
||||||
},
|
},
|
||||||
config: _.defaultsDeep(config, {
|
config: _.defaultsDeep({
|
||||||
validate: {
|
validate: {
|
||||||
params: {
|
params: joi.object().keys({
|
||||||
id: joi.any(),
|
id: joi.any(),
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
|
},
|
||||||
|
}, 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: path.join(prefix, model._plural, '{scope}'),
|
path: `${prefix}/${model._plural}/{scope}`,
|
||||||
|
|
||||||
@error
|
@error
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
const include = parseInclude(request);
|
const include = parseInclude(request);
|
||||||
const where = parseWhere(request);
|
const where = parseWhere(request);
|
||||||
|
|
||||||
if (include instanceof Error) return void reply(include);
|
|
||||||
|
|
||||||
const list = await model.scope(request.params.scope).findAll({ include, where });
|
const list = await model.scope(request.params.scope).findAll({ include, where });
|
||||||
|
|
||||||
reply(list);
|
reply(list);
|
||||||
},
|
},
|
||||||
config: _.defaultsDeep(config, {
|
config: _.defaultsDeep({
|
||||||
validate: {
|
validate: {
|
||||||
params: {
|
params: joi.object().keys({
|
||||||
scope: joi.string().valid(...scopes),
|
scope: joi.string().valid(...scopes),
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
|
},
|
||||||
|
}, config),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const create = ({ server, model, prefix = '/', config }) => {
|
export const create = ({server, model, prefix, config}) => {
|
||||||
server.route({
|
server.route({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: path.join(prefix, model._singular),
|
path: `${prefix}/${model._singular}`,
|
||||||
|
|
||||||
@error
|
@error
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
@ -224,10 +156,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: path.join(prefix, model._singular, '{id?}'),
|
path: `${prefix}/${model._singular}/{id?}`,
|
||||||
|
|
||||||
@error
|
@error
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
@ -245,10 +177,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: path.join(prefix, model._plural),
|
path: `${prefix}/${model._plural}`,
|
||||||
|
|
||||||
@error
|
@error
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
@ -265,45 +197,47 @@ 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: path.join(prefix, model._plural, '{scope}'),
|
path: `${prefix}/${model._plural}/{scope}`,
|
||||||
|
|
||||||
@error
|
@error
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
const include = parseInclude(request);
|
const include = parseInclude(request);
|
||||||
const where = parseWhere(request);
|
const where = parseWhere(request);
|
||||||
|
|
||||||
if (include instanceof Error) return void reply(include);
|
|
||||||
|
|
||||||
const list = await model.scope(request.params.scope).findAll({ include, where });
|
const list = await model.scope(request.params.scope).findAll({ include, where });
|
||||||
|
|
||||||
await Promise.all(list.map(instance => instance.destroy()));
|
await Promise.all(list.map(instance => instance.destroy()));
|
||||||
|
|
||||||
reply(list);
|
reply(list);
|
||||||
},
|
},
|
||||||
config: _.defaultsDeep(config, {
|
config: _.defaultsDeep({
|
||||||
validate: {
|
validate: {
|
||||||
params: {
|
params: joi.object().keys({
|
||||||
scope: joi.string().valid(...scopes),
|
scope: joi.string().valid(...scopes),
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
|
},
|
||||||
|
}, config),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const update = ({ server, model, prefix = '/', config }) => {
|
export const update = ({server, model, prefix, config}) => {
|
||||||
server.route({
|
server.route({
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
path: path.join(prefix, model._singular, '{id}'),
|
path: `${prefix}/${model._singular}/{id}`,
|
||||||
|
|
||||||
@error
|
@error
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
const { id } = request.params;
|
const { id } = request.params;
|
||||||
const instance = await model.findById(id);
|
const instance = await model.findOne({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (!instance) return void reply(notFound(`${id} not found.`));
|
if (!instance) return void reply(notFound(`${id} not found.`));
|
||||||
|
|
||||||
@ -312,17 +246,17 @@ export const update = ({ server, model, prefix = '/', config }) => {
|
|||||||
reply(instance);
|
reply(instance);
|
||||||
},
|
},
|
||||||
|
|
||||||
config: _.defaultsDeep(config, {
|
config: _.defaultsDeep({
|
||||||
validate: {
|
validate: {
|
||||||
payload: joi.object().required(),
|
payload: joi.object().required(),
|
||||||
params: {
|
|
||||||
id: joi.any(),
|
|
||||||
},
|
},
|
||||||
},
|
}, config),
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
import * as associations from './associations/index';
|
||||||
|
export { associations };
|
||||||
|
|
||||||
const methods = {
|
const methods = {
|
||||||
list, get, scope, create, destroy, destroyAll, destroyScope, update,
|
list, get, scope, create, destroy, destroyAll, destroyScope, update,
|
||||||
};
|
};
|
||||||
|
146
src/crud.test.js
146
src/crud.test.js
@ -1,146 +0,0 @@
|
|||||||
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'
|
|
||||||
);
|
|
||||||
});
|
|
55
src/error.js
55
src/error.js
@ -1,5 +1,3 @@
|
|||||||
import Boom from 'boom';
|
|
||||||
|
|
||||||
export default (target, key, descriptor) => {
|
export default (target, key, descriptor) => {
|
||||||
const fn = descriptor.value;
|
const fn = descriptor.value;
|
||||||
|
|
||||||
@ -7,60 +5,9 @@ export default (target, key, descriptor) => {
|
|||||||
try {
|
try {
|
||||||
await fn(request, reply);
|
await fn(request, reply);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.original) {
|
console.error(e);
|
||||||
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);
|
reply(e);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return descriptor;
|
return descriptor;
|
||||||
|
@ -1,88 +0,0 @@
|
|||||||
import { defaultsDeep } from 'lodash';
|
|
||||||
import joi from 'joi';
|
|
||||||
|
|
||||||
export const sequelizeOperators = {
|
|
||||||
$and: joi.any(),
|
|
||||||
$or: joi.any(),
|
|
||||||
$gt: joi.any(),
|
|
||||||
$gte: joi.any(),
|
|
||||||
$lt: joi.any(),
|
|
||||||
$lte: joi.any(),
|
|
||||||
$ne: joi.any(),
|
|
||||||
$eq: joi.any(),
|
|
||||||
$not: joi.any(),
|
|
||||||
$between: joi.any(),
|
|
||||||
$notBetween: joi.any(),
|
|
||||||
$in: joi.any(),
|
|
||||||
$notIn: joi.any(),
|
|
||||||
$like: joi.any(),
|
|
||||||
$notLike: joi.any(),
|
|
||||||
$iLike: joi.any(),
|
|
||||||
$notILike: joi.any(),
|
|
||||||
$overlap: joi.any(),
|
|
||||||
$contains: joi.any(),
|
|
||||||
$contained: joi.any(),
|
|
||||||
$any: joi.any(),
|
|
||||||
$col: joi.any(),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const whereMethods = [
|
|
||||||
'list',
|
|
||||||
'get',
|
|
||||||
'scope',
|
|
||||||
'destroy',
|
|
||||||
'destoryScope',
|
|
||||||
'destroyAll',
|
|
||||||
];
|
|
||||||
|
|
||||||
export const includeMethods = [
|
|
||||||
'list',
|
|
||||||
'get',
|
|
||||||
'scope',
|
|
||||||
'destoryScope',
|
|
||||||
];
|
|
||||||
|
|
||||||
export const payloadMethods = [
|
|
||||||
'create',
|
|
||||||
'update',
|
|
||||||
];
|
|
||||||
|
|
||||||
export default ({ method, attributeValidation, associationValidation, config = {} }) => {
|
|
||||||
const hasWhere = whereMethods.includes(method);
|
|
||||||
const hasInclude = includeMethods.includes(method);
|
|
||||||
const hasPayload = payloadMethods.includes(method);
|
|
||||||
const methodConfig = { ...config };
|
|
||||||
|
|
||||||
if (hasWhere) {
|
|
||||||
defaultsDeep(methodConfig, {
|
|
||||||
validate: {
|
|
||||||
query: {
|
|
||||||
...attributeValidation,
|
|
||||||
...sequelizeOperators,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasInclude) {
|
|
||||||
defaultsDeep(methodConfig, {
|
|
||||||
validate: {
|
|
||||||
query: {
|
|
||||||
...associationValidation,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasPayload) {
|
|
||||||
defaultsDeep(methodConfig, {
|
|
||||||
validate: {
|
|
||||||
payload: {
|
|
||||||
...attributeValidation,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return methodConfig;
|
|
||||||
};
|
|
@ -1,117 +0,0 @@
|
|||||||
import test from 'ava';
|
|
||||||
import joi from 'joi';
|
|
||||||
import
|
|
||||||
getConfigForMethod, {
|
|
||||||
whereMethods,
|
|
||||||
includeMethods,
|
|
||||||
payloadMethods,
|
|
||||||
sequelizeOperators,
|
|
||||||
} from './get-config-for-method.js';
|
|
||||||
|
|
||||||
test.beforeEach((t) => {
|
|
||||||
t.context.attributeValidation = {
|
|
||||||
myKey: joi.any(),
|
|
||||||
};
|
|
||||||
|
|
||||||
t.context.associationValidation = {
|
|
||||||
include: ['MyModel'],
|
|
||||||
};
|
|
||||||
|
|
||||||
t.context.config = {
|
|
||||||
cors: {},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
test('get-config-for-method validate.query seqeulizeOperators', (t) => {
|
|
||||||
whereMethods.forEach((method) => {
|
|
||||||
const configForMethod = getConfigForMethod({ method });
|
|
||||||
const { query } = configForMethod.validate;
|
|
||||||
const configForMethodValidateQueryKeys = Object.keys(query);
|
|
||||||
|
|
||||||
t.truthy(
|
|
||||||
query,
|
|
||||||
`applies query validation for ${method}`
|
|
||||||
);
|
|
||||||
|
|
||||||
Object.keys(sequelizeOperators).forEach((operator) => {
|
|
||||||
t.truthy(
|
|
||||||
configForMethodValidateQueryKeys.includes(operator),
|
|
||||||
`applies sequelize operator "${operator}" in validate.where for ${method}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('get-config-for-method validate.query attributeValidation', (t) => {
|
|
||||||
const { attributeValidation } = t.context;
|
|
||||||
|
|
||||||
whereMethods.forEach((method) => {
|
|
||||||
const configForMethod = getConfigForMethod({ method, attributeValidation });
|
|
||||||
const { query } = configForMethod.validate;
|
|
||||||
|
|
||||||
Object.keys(attributeValidation).forEach((key) => {
|
|
||||||
t.truthy(
|
|
||||||
query[key]
|
|
||||||
, `applies attributeValidation (${key}) to validate.query`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('get-config-for-method validate.query associationValidation', (t) => {
|
|
||||||
const { attributeValidation, associationValidation } = t.context;
|
|
||||||
|
|
||||||
includeMethods.forEach((method) => {
|
|
||||||
const configForMethod = getConfigForMethod({
|
|
||||||
method,
|
|
||||||
attributeValidation,
|
|
||||||
associationValidation,
|
|
||||||
});
|
|
||||||
const { query } = configForMethod.validate;
|
|
||||||
|
|
||||||
Object.keys(attributeValidation).forEach((key) => {
|
|
||||||
t.truthy(
|
|
||||||
query[key]
|
|
||||||
, `applies attributeValidation (${key}) to validate.query when include should be applied`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.keys(associationValidation).forEach((key) => {
|
|
||||||
t.truthy(
|
|
||||||
query[key]
|
|
||||||
, `applies associationValidation (${key}) to validate.query when include should be applied`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('get-config-for-method validate.payload associationValidation', (t) => {
|
|
||||||
const { attributeValidation } = t.context;
|
|
||||||
|
|
||||||
payloadMethods.forEach((method) => {
|
|
||||||
const configForMethod = getConfigForMethod({ method, attributeValidation });
|
|
||||||
const { payload } = configForMethod.validate;
|
|
||||||
|
|
||||||
Object.keys(attributeValidation).forEach((key) => {
|
|
||||||
t.truthy(
|
|
||||||
payload[key]
|
|
||||||
, `applies attributeValidation (${key}) to validate.payload`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('get-config-for-method does not modify initial config on multiple passes', (t) => {
|
|
||||||
const { config } = t.context;
|
|
||||||
const originalConfig = { ...config };
|
|
||||||
|
|
||||||
whereMethods.forEach((method) => {
|
|
||||||
getConfigForMethod({ method, config });
|
|
||||||
});
|
|
||||||
|
|
||||||
t.deepEqual(
|
|
||||||
config
|
|
||||||
, originalConfig
|
|
||||||
, 'does not modify the original config object'
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,5 +1,5 @@
|
|||||||
if (!global._babelPolyfill) {
|
if (!global._babelPolyfill) {
|
||||||
require('babel-polyfill');
|
require('babel/polyfill');
|
||||||
}
|
}
|
||||||
|
|
||||||
import crud, { associations } from './crud';
|
import crud, { associations } from './crud';
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
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
|
||||||
@ -9,7 +8,7 @@ export const parseInclude = request => {
|
|||||||
const noRequestModels = !request.models;
|
const noRequestModels = !request.models;
|
||||||
|
|
||||||
if (noGetDb && noRequestModels) {
|
if (noGetDb && noRequestModels) {
|
||||||
return notImplemented('`request.getDb` or `request.models` are not defined.'
|
return new Error('`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.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user