33 Commits

Author SHA1 Message Date
a720e30a85 2.6.2 2016-09-08 13:27:20 -07:00
518c4a4226 2.6.1 2016-09-08 13:27:10 -07:00
469aaec66f Merge pull request #23 from mdibaiee/fix-joi-concat
Fix (validation) use joi's concat
2016-09-08 13:26:50 -07:00
8ee5661252 Test: only run src test files #oops 2016-09-08 13:24:08 -07:00
c59943a717 Fix (validation) use joi's concat
It turns out defaultsDeep doesn't ever correctly combine Joi objects.
So, the only option is to use Joi's concat method to combine Joi
schemas. This complicates `getConfigForMethod`, but simplifies actual
route creation.

I ran into this because I'm setting up [lout](https://github.com/hapijs/lout)
on a server, and it requires properly formatted Joi schemas. This leads
me to believe there was something already wrong and Lout just exposed
the problem.
2016-09-08 13:20:50 -07:00
8cdfc5858d Merge pull request #24 from mdibaiee/use-json
Fix toJSON responses
2016-09-08 11:33:47 -07:00
4e078f5ba5 Fix toJSON responses
This is a non-obvious one. Hapi is happy to convert raw sequelize
instances to proper JSON (likely because Sequelize does nice things),
but we do that, we can't use `config.response.schema`, because it
receives the full sequelize instance instead of JSON.

This is a patch release.
2016-09-07 21:06:34 -07:00
85111c7dc8 2.6.0 2016-09-06 11:30:22 -07:00
196999a4c5 Merge pull request #20 from Getable/error-on-invalid-where
Validate query and payload
2016-09-06 11:29:40 -07:00
3e9f024dcf Test: now testing get-config-for-method 2016-09-06 11:25:03 -07:00
4c9ae36c5c Refactor: move get-config-for-method to a file 2016-09-06 11:24:41 -07:00
4558ad1327 Chore (deps) update minors and patches
Not strictly necessary, but kinda nice to prove we're up-to-date
2016-09-06 09:25:02 -07:00
edccfb2316 Chore (deps) update Joi 7 → 9
Shouldn't impact us
2016-09-06 07:28:43 -07:00
0d8ab9f02e Chore (deps) update boom (major)
They just removed a method we don't use.
2016-09-06 07:28:43 -07:00
f062e2b37f Fix (validation) params is a plain object
If we use a Joi object here, we can't use `defaultsDeep` to extend b/c
the joi prototype won't extend cleanly. We'd need to use joi's `contact`
method, but that gets really complicated and error prone. So, just use
a plain object which is more correct anyway.

http://hapijs.com/tutorials/validation
2016-09-06 07:28:43 -07:00
69221ea331 Feat query & payload now validated 2016-09-06 07:28:43 -07:00
f33c8da55d Fix (CRUD update) validate id 2016-09-06 07:28:43 -07:00
833df49173 Chore add comments for config creation 2016-09-06 07:28:43 -07:00
32a539c3d9 Fix (crud) update: findOnefindById
b/c `findById` uses an index to lookup, and should be fast.
2016-09-06 07:28:43 -07:00
b35bd23c91 Fix: prefer user's config before our own 2016-09-06 07:28:43 -07:00
b4ea8c5b8e Docs: add more details for include and where 2016-09-06 07:28:43 -07:00
85cd2823da Docs: #cleanup and style fixes 2016-09-06 07:28:43 -07:00
e0132c2cae Fix: handle all parseInclude errors 2016-09-06 07:28:43 -07:00
bd18c57529 chore: put CircleCI badge in the same line as heading 2016-09-06 09:30:21 +04:30
3e53ba8d2c Merge branch 'master' of github.com:mdibaiee/hapi-sequelize-crud 2016-09-06 09:29:39 +04:30
81b704a395 chore: add CircleCI badge 2016-09-06 09:28:56 +04:30
d69b87b8fa Merge pull request #22 from mdibaiee/tests
Add Tests
2016-09-06 09:25:24 +04:30
03755f94c5 Test (CI) configure circle 2016-09-05 17:37:50 -07:00
7cecd7fb40 Test (list) add initial list tests 2016-09-05 17:12:13 -07:00
7b757fcc50 Fix (crud) if no prefix, things still work 2016-09-05 17:11:42 -07:00
de0685c8bb Chore: install and configure AVA 2016-09-05 17:10:58 -07:00
f2f613b35b Fix: boom error on invalid include
Sends a 501 `notImplemented` error when `parseInclude` can't find models
to include.
2016-09-05 17:08:09 -07:00
38ccb3adf6 Chore (deps) update eslint (major)
Breaking changes shouldn't affect us
2016-09-05 15:43:48 -07:00
9 changed files with 986 additions and 93 deletions

View File

@ -1,3 +1,9 @@
{
"extends": "pichak"
"plugins": [
"ava"
],
"extends": [
"pichak",
"plugin:ava/recommended"
]
}

View File

@ -1,4 +1,4 @@
hapi-sequelize-crud
hapi-sequelize-crud [![CircleCI](https://circleci.com/gh/mdibaiee/hapi-sequelize-crud.svg?style=svg)](https://circleci.com/gh/mdibaiee/hapi-sequelize-crud)
===================
Automatically generate a RESTful API for your models and associations
@ -11,6 +11,9 @@ npm install -S hapi-sequelize-crud
##Configure
Please note that you should register `hapi-sequelize-crud` after defining your
associations.
```javascript
// First, register hapi-sequelize
await register({
@ -35,6 +38,7 @@ await register({
// `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
@ -54,20 +58,59 @@ await register({
```
### 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
* **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
## `where` queries
It's easy to restrict your requests using Sequelize's `where` query option. Just pass a query parameter.
Please note that you should register `hapi-sequelize-crud` after defining your
associations.
```js
// returns only teams that have a `city` property of "windsor"
// GET /team?city=windsor
##What do I get
// results in the Sequelize query:
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:
@ -82,8 +125,9 @@ You get these:
# get an array of records
GET /team/{id}/roles
GET /role/{id}/teams
# might also append query parameters to search for
# might also append `where` query parameters to search for
GET /role/{id}/teams?members=5
GET /role/{id}/teams?city=healdsburg
# you might also use scopes
GET /teams/{scope}/roles/{scope}

9
circle.yml Normal file
View File

@ -0,0 +1,9 @@
machine:
node:
version: 6.5.0
dependencies:
pre:
- npm prune
post:
- mkdir -p $CIRCLE_TEST_REPORTS/ava

View File

@ -1,6 +1,6 @@
{
"name": "hapi-sequelize-crud",
"version": "2.5.4",
"version": "2.6.2",
"description": "Hapi plugin that automatically generates RESTful API for CRUD",
"main": "build/index.js",
"config": {
@ -9,8 +9,9 @@
}
},
"scripts": {
"lint": "eslint src test",
"test": "echo \"Error: no test specified\" && exit 1",
"lint": "eslint src",
"test": "ava --require babel-register --source='src/**/*.js' --source='!build/**/*' --tap=${CI-false} src/**/*.test.js | $(if [ -z ${CI:-} ]; then echo 'tail'; else tap-xunit > $CIRCLE_TEST_REPORTS/ava/ava.xml; fi;)",
"tdd": "ava --require babel-register --source='src/**/*.js' --source='!build/**/*' --watch src/**/*.test.js",
"build": "scripty",
"watch": "scripty"
},
@ -23,21 +24,27 @@
"author": "Mahdi Dibaiee <mdibaiee@aol.com> (http://dibaiee.ir/)",
"license": "MIT",
"devDependencies": {
"babel-cli": "^6.10.1",
"ava": "^0.16.0",
"babel-cli": "^6.14.0",
"babel-plugin-add-module-exports": "^0.2.1",
"babel-plugin-closure-elimination": "^1.0.6",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-plugin-transform-es2015-modules-commonjs": "^6.10.3",
"babel-preset-stage-1": "^6.5.0",
"eslint": "2.10.2",
"eslint-config-pichak": "1.1.0",
"ghooks": "1.0.3",
"scripty": "^1.6.0"
"babel-plugin-transform-es2015-modules-commonjs": "^6.14.0",
"babel-preset-stage-1": "^6.13.0",
"babel-register": "^6.14.0",
"eslint": "^3.4.0",
"eslint-config-pichak": "^1.1.2",
"eslint-plugin-ava": "^3.0.0",
"ghooks": "^1.3.2",
"scripty": "^1.6.0",
"sinon": "^1.17.5",
"sinon-bluebird": "^3.0.2",
"tap-xunit": "^1.4.0"
},
"dependencies": {
"boom": "^3.2.2",
"joi": "7.2.1",
"lodash": "4.0.0"
"boom": "^4.0.0",
"joi": "^9.0.4",
"lodash": "^4.15.0"
},
"optionalDependencies": {
"babel-polyfill": "^6.13.0"

View File

@ -1,13 +1,34 @@
import joi from 'joi';
import path from 'path';
import error from './error';
import _ from 'lodash';
import { parseInclude, parseWhere } from './utils';
import { notFound } from 'boom';
import * as associations from './associations/index';
import getConfigForMethod from './get-config-for-method.js';
const createAll = ({ server, model, prefix, config }) => {
const createAll = ({
server,
model,
prefix,
config,
attributeValidation,
associationValidation,
scopes,
}) => {
Object.keys(methods).forEach((method) => {
methods[method]({ server, model, prefix, config });
methods[method]({
server,
model,
prefix,
config: getConfigForMethod({
method,
attributeValidation,
associationValidation,
config,
scopes,
}),
});
});
};
@ -33,13 +54,47 @@ models: {
export default (server, model, { prefix, defaultConfig: config, models: permissions }) => {
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)),
};
const scopes = Object.keys(model.options.scopes);
// if we don't have any permissions set, just create all the methods
if (!permissions) {
createAll({ server, model, prefix, config });
createAll({
server,
model,
prefix,
config,
attributeValidation,
associationValidation,
scopes,
});
// 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 });
createAll({
server,
model,
prefix,
config,
attributeValidation,
associationValidation,
scopes,
});
// if we've gotten here, we have complex permissions and need to set them
} else {
const permissionOptions = permissions.filter((permission) => {
return permission.model === modelName;
@ -55,21 +110,35 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi
server,
model,
prefix,
config: permissionConfig,
config: getConfigForMethod({
method,
attributeValidation,
associationValidation,
scopes,
config: permissionConfig,
}),
});
});
} else {
createAll({ server, model, prefix, config: permissionConfig });
createAll({
server,
model,
prefix,
attributeValidation,
associationValidation,
scopes,
config: permissionConfig,
});
}
}
});
}
};
export const list = ({ server, model, prefix, config }) => {
export const list = ({ server, model, prefix = '/', config }) => {
server.route({
method: 'GET',
path: `${prefix}/${model._plural}`,
path: path.join(prefix, model._plural),
@error
async handler(request, reply) {
@ -82,17 +151,17 @@ export const list = ({ server, model, prefix, config }) => {
where, include,
});
reply(list);
reply(list.map((item) => item.toJSON()));
},
config,
});
};
export const get = ({ server, model, prefix, config }) => {
export const get = ({ server, model, prefix = '/', config }) => {
server.route({
method: 'GET',
path: `${prefix}/${model._singular}/{id?}`,
path: path.join(prefix, model._singular, '{id?}'),
@error
async handler(request, reply) {
@ -101,68 +170,58 @@ export const get = ({ server, model, prefix, config }) => {
const { id } = request.params;
if (id) where[model.primaryKeyField] = id;
if (include instanceof Error) return void reply(include);
const instance = await model.findOne({ where, include });
if (!instance) return void reply(notFound(`${id} not found.`));
reply(instance);
reply(instance.toJSON());
},
config: _.defaultsDeep({
validate: {
params: joi.object().keys({
id: joi.any(),
}),
},
}, config),
config,
});
};
export const scope = ({ server, model, prefix, config }) => {
const scopes = Object.keys(model.options.scopes);
export const scope = ({ server, model, prefix = '/', config }) => {
server.route({
method: 'GET',
path: `${prefix}/${model._plural}/{scope}`,
path: path.join(prefix, model._plural, '{scope}'),
@error
async handler(request, reply) {
const include = parseInclude(request);
const where = parseWhere(request);
if (include instanceof Error) return void reply(include);
const list = await model.scope(request.params.scope).findAll({ include, where });
reply(list);
reply(list.map((item) => item.toJSON()));
},
config: _.defaultsDeep({
validate: {
params: joi.object().keys({
scope: joi.string().valid(...scopes),
}),
},
}, config),
config,
});
};
export const create = ({ server, model, prefix, config }) => {
export const create = ({ server, model, prefix = '/', config }) => {
server.route({
method: 'POST',
path: `${prefix}/${model._singular}`,
path: path.join(prefix, model._singular),
@error
async handler(request, reply) {
const instance = await model.create(request.payload);
reply(instance);
reply(instance.toJSON());
},
config,
});
};
export const destroy = ({ server, model, prefix, config }) => {
export const destroy = ({ server, model, prefix = '/', config }) => {
server.route({
method: 'DELETE',
path: `${prefix}/${model._singular}/{id?}`,
path: path.join(prefix, model._singular, '{id?}'),
@error
async handler(request, reply) {
@ -173,17 +232,18 @@ export const destroy = ({ server, model, prefix, config }) => {
await Promise.all(list.map(instance => instance.destroy()));
reply(list.length === 1 ? list[0] : list);
const listAsJSON = list.map((item) => item.toJSON());
reply(listAsJSON.length === 1 ? listAsJSON[0] : listAsJSON);
},
config,
});
};
export const destroyAll = ({ server, model, prefix, config }) => {
export const destroyAll = ({ server, model, prefix = '/', config }) => {
server.route({
method: 'DELETE',
path: `${prefix}/${model._plural}`,
path: path.join(prefix, model._plural),
@error
async handler(request, reply) {
@ -193,67 +253,55 @@ export const destroyAll = ({ server, model, prefix, config }) => {
await Promise.all(list.map(instance => instance.destroy()));
reply(list.length === 1 ? list[0] : list);
const listAsJSON = list.map((item) => item.toJSON());
reply(listAsJSON.length === 1 ? listAsJSON[0] : listAsJSON);
},
config,
});
};
export const destroyScope = ({ server, model, prefix, config }) => {
const scopes = Object.keys(model.options.scopes);
export const destroyScope = ({ server, model, prefix = '/', config }) => {
server.route({
method: 'DELETE',
path: `${prefix}/${model._plural}/{scope}`,
path: path.join(prefix, model._plural, '{scope}'),
@error
async handler(request, reply) {
const include = parseInclude(request);
const where = parseWhere(request);
if (include instanceof Error) return void reply(include);
const list = await model.scope(request.params.scope).findAll({ include, where });
await Promise.all(list.map(instance => instance.destroy()));
reply(list);
const listAsJSON = list.map((item) => item.toJSON());
reply(listAsJSON.length === 1 ? listAsJSON[0] : listAsJSON);
},
config: _.defaultsDeep({
validate: {
params: joi.object().keys({
scope: joi.string().valid(...scopes),
}),
},
}, config),
config,
});
};
export const update = ({ server, model, prefix, config }) => {
export const update = ({ server, model, prefix = '/', config }) => {
server.route({
method: 'PUT',
path: `${prefix}/${model._singular}/{id}`,
path: path.join(prefix, model._singular, '{id}'),
@error
async handler(request, reply) {
const { id } = request.params;
const instance = await model.findOne({
where: {
id,
},
});
const instance = await model.findById(id);
if (!instance) return void reply(notFound(`${id} not found.`));
await instance.update(request.payload);
reply(instance);
reply(instance.toJSON());
},
config: _.defaultsDeep({
validate: {
payload: joi.object().required(),
},
}, config),
config,
});
};

157
src/crud.test.js Normal file
View File

@ -0,0 +1,157 @@
import test from 'ava';
import { list } from './crud.js';
import { stub } from 'sinon';
import uniqueId from 'lodash/uniqueId.js';
import 'sinon-bluebird';
const METHODS = {
GET: 'GET',
};
test.beforeEach('setup server', (t) => {
t.context.server = {
route: stub(),
};
});
const makeModel = () => {
const id = uniqueId();
return {
findAll: stub(),
_plural: 'models',
_singular: 'model',
toJSON: () => ({ id }),
id,
};
};
test.beforeEach('setup model', (t) => {
t.context.model = makeModel();
});
test.beforeEach('setup models', (t) => {
t.context.models = [t.context.model, makeModel()];
});
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, models } = t.context;
list({ server, model });
const { handler } = server.route.args[0][0];
model.findAll.resolves(models);
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.deepEqual(
response,
models.map(({ id }) => ({ id })),
'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'
);
});

View File

@ -0,0 +1,137 @@
import { set, get } from 'lodash';
import joi from 'joi';
// if the custom validation is a joi object we need to concat
// else, assume it's an plain object and we can just add it in with .keys
const concatToJoiObject = (joi, candidate) => {
if (!candidate) return joi;
else if (candidate.isJoi) return joi.concat(candidate);
else return joi.keys(candidate);
};
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 const scopeParamsMethods = [
'destroyScope',
'scope',
];
export const idParamsMethods = [
'get',
'update',
];
export default ({
method, attributeValidation, associationValidation, scopes = [], config = {},
}) => {
const hasWhere = whereMethods.includes(method);
const hasInclude = includeMethods.includes(method);
const hasPayload = payloadMethods.includes(method);
const hasScopeParams = scopeParamsMethods.includes(method);
const hasIdParams = idParamsMethods.includes(method);
// clone the config so we don't modify it on multiple passes.
let methodConfig = { ...config, validate: { ...config.validate } };
if (hasWhere) {
const query = concatToJoiObject(joi.object()
.keys({
...attributeValidation,
...sequelizeOperators,
}),
get(methodConfig, 'validate.query')
);
methodConfig = set(methodConfig, 'validate.query', query);
}
if (hasInclude) {
const query = concatToJoiObject(joi.object()
.keys({
...associationValidation,
}),
get(methodConfig, 'validate.query')
);
methodConfig = set(methodConfig, 'validate.query', query);
}
if (hasPayload) {
const payload = concatToJoiObject(joi.object()
.keys({
...attributeValidation,
}),
get(methodConfig, 'validate.payload')
);
methodConfig = set(methodConfig, 'validate.payload', payload);
}
if (hasScopeParams) {
const params = concatToJoiObject(joi.object()
.keys({
scope: joi.string().valid(...scopes),
}),
get(methodConfig, 'validate.params')
);
methodConfig = set(methodConfig, 'validate.params', params);
}
if (hasIdParams) {
const params = concatToJoiObject(joi.object()
.keys({
id: joi.any(),
}),
get(methodConfig, 'validate.params')
);
methodConfig = set(methodConfig, 'validate.params', params);
}
return methodConfig;
};

View File

@ -0,0 +1,484 @@
import test from 'ava';
import joi from 'joi';
import
getConfigForMethod, {
whereMethods,
includeMethods,
payloadMethods,
scopeParamsMethods,
idParamsMethods,
sequelizeOperators,
} from './get-config-for-method.js';
test.beforeEach((t) => {
t.context.models = ['MyModel'];
t.context.scopes = ['aScope'];
t.context.attributeValidation = {
myKey: joi.any(),
};
t.context.associationValidation = {
include: joi.array().items(joi.string().valid(t.context.models)),
};
t.context.config = {
cors: {},
};
});
test('validate.query seqeulizeOperators', (t) => {
whereMethods.forEach((method) => {
const configForMethod = getConfigForMethod({ method });
const { query } = configForMethod.validate;
t.truthy(
query,
`applies query validation for ${method}`
);
Object.keys(sequelizeOperators).forEach((operator) => {
t.ifError(
query.validate({ [operator]: true }).error
, `applies sequelize operator "${operator}" in validate.where for ${method}`
);
});
t.truthy(
query.validate({ notAThing: true }).error
, 'errors on a non-valid key'
);
});
});
test('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.ifError(
query.validate({ [key]: true }).error
, `applies attributeValidation (${key}) to validate.query`
);
});
t.truthy(
query.validate({ notAThing: true }).error
, 'errors on a non-valid key'
);
});
});
test('query attributeValidation w/ config as plain object', (t) => {
const { attributeValidation } = t.context;
const config = {
validate: {
query: {
aKey: joi.boolean(),
},
},
};
whereMethods.forEach((method) => {
const configForMethod = getConfigForMethod({
method,
attributeValidation,
config,
});
const { query } = configForMethod.validate;
const keys = [
...Object.keys(attributeValidation),
...Object.keys(config.validate.query),
];
keys.forEach((key) => {
t.ifError(
query.validate({ [key]: true }).error
, `applies ${key} to validate.query`
);
});
t.truthy(
query.validate({ notAThing: true }).error
, 'errors on a non-valid key'
);
});
});
test('query attributeValidation w/ config as joi object', (t) => {
const { attributeValidation } = t.context;
const queryKeys = {
aKey: joi.boolean(),
};
const config = {
validate: {
query: joi.object().keys(queryKeys),
},
};
whereMethods.forEach((method) => {
const configForMethod = getConfigForMethod({
method,
attributeValidation,
config,
});
const { query } = configForMethod.validate;
const keys = [
...Object.keys(attributeValidation),
...Object.keys(queryKeys),
];
keys.forEach((key) => {
t.ifError(
query.validate({ [key]: true }).error
, `applies ${key} to validate.query`
);
});
t.truthy(
query.validate({ notAThing: true }).error
, 'errors on a non-valid key'
);
});
});
test('validate.query associationValidation', (t) => {
const { attributeValidation, associationValidation, models } = t.context;
includeMethods.forEach((method) => {
const configForMethod = getConfigForMethod({
method,
attributeValidation,
associationValidation,
});
const { query } = configForMethod.validate;
Object.keys(attributeValidation).forEach((key) => {
t.ifError(
query.validate({ [key]: true }).error
, `applies attributeValidation (${key}) to validate.query when include should be applied`
);
});
Object.keys(associationValidation).forEach((key) => {
t.ifError(
query.validate({ [key]: models }).error
, `applies associationValidation (${key}) to validate.query when include should be applied`
);
});
t.truthy(
query.validate({ notAThing: true }).error
, 'errors on a non-valid key'
);
});
});
test('query associationValidation w/ config as plain object', (t) => {
const { associationValidation, models } = t.context;
const config = {
validate: {
query: {
aKey: joi.boolean(),
},
},
};
includeMethods.forEach((method) => {
const configForMethod = getConfigForMethod({
method,
associationValidation,
config,
});
const { query } = configForMethod.validate;
Object.keys(associationValidation).forEach((key) => {
t.ifError(
query.validate({ [key]: models }).error
, `applies ${key} to validate.query`
);
});
Object.keys(config.validate.query).forEach((key) => {
t.ifError(
query.validate({ [key]: true }).error
, `applies ${key} to validate.query`
);
});
t.truthy(
query.validate({ notAThing: true }).error
, 'errors on a non-valid key'
);
});
});
test('query associationValidation w/ config as joi object', (t) => {
const { associationValidation, models } = t.context;
const queryKeys = {
aKey: joi.boolean(),
};
const config = {
validate: {
query: joi.object().keys(queryKeys),
},
};
includeMethods.forEach((method) => {
const configForMethod = getConfigForMethod({
method,
associationValidation,
config,
});
const { query } = configForMethod.validate;
Object.keys(associationValidation).forEach((key) => {
t.ifError(
query.validate({ [key]: models }).error
, `applies ${key} to validate.query`
);
});
Object.keys(queryKeys).forEach((key) => {
t.ifError(
query.validate({ [key]: true }).error
, `applies ${key} to validate.query`
);
});
t.truthy(
query.validate({ notAThing: true }).error
, 'errors on a non-valid key'
);
});
});
test('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.ifError(
payload.validate({ [key]: true }).error
, `applies attributeValidation (${key}) to validate.payload`
);
});
t.truthy(
payload.validate({ notAThing: true }).error
, 'errors on a non-valid key'
);
});
});
test('payload attributeValidation w/ config as plain object', (t) => {
const { attributeValidation } = t.context;
const config = {
validate: {
payload: {
aKey: joi.boolean(),
},
},
};
payloadMethods.forEach((method) => {
const configForMethod = getConfigForMethod({
method,
attributeValidation,
config,
});
const { payload } = configForMethod.validate;
const keys = [
...Object.keys(attributeValidation),
...Object.keys(config.validate.payload),
];
keys.forEach((key) => {
t.ifError(
payload.validate({ [key]: true }).error
, `applies ${key} to validate.payload`
);
});
t.truthy(
payload.validate({ notAThing: true }).error
, 'errors on a non-valid key'
);
});
});
test('payload attributeValidation w/ config as joi object', (t) => {
const { attributeValidation } = t.context;
const payloadKeys = {
aKey: joi.boolean(),
};
const config = {
validate: {
payload: joi.object().keys(payloadKeys),
},
};
payloadMethods.forEach((method) => {
const configForMethod = getConfigForMethod({
method,
attributeValidation,
config,
});
const { payload } = configForMethod.validate;
const keys = [
...Object.keys(attributeValidation),
...Object.keys(payloadKeys),
];
keys.forEach((key) => {
t.ifError(
payload.validate({ [key]: true }).error
, `applies ${key} to validate.payload`
);
});
t.truthy(
payload.validate({ notAThing: true }).error
, 'errors on a non-valid key'
);
});
});
test('validate.params scopeParamsMethods', (t) => {
const { scopes } = t.context;
scopeParamsMethods.forEach((method) => {
const configForMethod = getConfigForMethod({ method, scopes });
const { params } = configForMethod.validate;
scopes.forEach((key) => {
t.ifError(
params.validate({ scope: key }).error
, `applies "scope: ${key}" to validate.params`
);
});
t.truthy(
params.validate({ scope: 'notAthing' }).error
, 'errors on a non-valid key'
);
});
});
test('params scopeParamsMethods w/ config as plain object', (t) => {
const { scopes } = t.context;
const config = {
validate: {
params: {
aKey: joi.boolean(),
},
},
};
scopeParamsMethods.forEach((method) => {
const configForMethod = getConfigForMethod({
method,
scopes,
config,
});
const { params } = configForMethod.validate;
scopes.forEach((key) => {
t.ifError(
params.validate({ scope: key }).error
, `applies "scope: ${key}" to validate.params`
);
});
Object.keys(config.validate.params).forEach((key) => {
t.ifError(
params.validate({ [key]: true }).error
, `applies ${key} to validate.params`
);
});
t.truthy(
params.validate({ notAThing: true }).error
, 'errors on a non-valid key'
);
});
});
test('params scopeParamsMethods w/ config as joi object', (t) => {
const { scopes } = t.context;
const paramsKeys = {
aKey: joi.boolean(),
};
const config = {
validate: {
params: joi.object().keys(paramsKeys),
},
};
scopeParamsMethods.forEach((method) => {
const configForMethod = getConfigForMethod({
method,
scopes,
config,
});
const { params } = configForMethod.validate;
scopes.forEach((key) => {
t.ifError(
params.validate({ scope: key }).error
, `applies "scope: ${key}" to validate.params`
);
});
Object.keys(paramsKeys).forEach((key) => {
t.ifError(
params.validate({ [key]: true }).error
, `applies ${key} to validate.params`
);
});
t.truthy(
params.validate({ notAThing: true }).error
, 'errors on a non-valid key'
);
});
});
test('validate.payload idParamsMethods', (t) => {
idParamsMethods.forEach((method) => {
const configForMethod = getConfigForMethod({ method });
const { params } = configForMethod.validate;
t.ifError(
params.validate({ id: 'aThing' }).error
, 'applies id to validate.params'
);
});
});
test('does not modify initial config on multiple passes', (t) => {
const { config } = t.context;
const originalConfig = { ...config };
whereMethods.forEach((method) => {
getConfigForMethod({ method, ...t.context });
});
t.deepEqual(
config
, originalConfig
, 'does not modify the original config object'
);
});

View File

@ -1,4 +1,5 @@
import { omit, identity } from 'lodash';
import { notImplemented } from 'boom';
export const parseInclude = request => {
const include = Array.isArray(request.query.include) ? request.query.include
@ -8,7 +9,7 @@ export const parseInclude = request => {
const noRequestModels = !request.models;
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.');
}