Joey Baker 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

414 lines
9.1 KiB
JavaScript

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';
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(),
};
const whereMethods = [
'list',
'get',
'scope',
'destroy',
'destoryScope',
'destroyAll',
];
const includeMethods = [
'list',
'get',
'scope',
'destoryScope',
];
const payloadMethods = [
'create',
'update',
];
const getConfigForMethod = ({ 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;
};
const createAll = ({
server,
model,
prefix,
config,
attributeValidation,
associationValidation,
}) => {
Object.keys(methods).forEach((method) => {
methods[method]({
server,
model,
prefix,
config: getConfigForMethod({
method,
attributeValidation,
associationValidation,
config,
}),
});
});
};
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;
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) {
createAll({
server,
model,
prefix,
config,
attributeValidation,
associationValidation,
});
// 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 (_.isPlainObject(permissionOption)) {
const permissionConfig = permissionOption.config || config;
if (permissionOption.methods) {
permissionOption.methods.forEach((method) => {
methods[method]({
server,
model,
prefix,
config: getConfigForMethod({
method,
attributeValidation,
associationValidation,
config: permissionConfig,
}),
});
});
} else {
createAll({
server,
model,
prefix,
attributeValidation,
associationValidation,
config: permissionConfig,
});
}
}
});
}
};
export const list = ({ server, model, prefix = '/', config }) => {
server.route({
method: 'GET',
path: path.join(prefix, model._plural),
@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.findAll({
where, include,
});
reply(list);
},
config,
});
};
export const get = ({ server, model, prefix = '/', config }) => {
server.route({
method: 'GET',
path: path.join(prefix, model._singular, '{id?}'),
@error
async handler(request, reply) {
const include = parseInclude(request);
const where = parseWhere(request);
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);
},
config: _.defaultsDeep(config, {
validate: {
params: {
id: joi.any(),
},
},
}),
});
};
export const scope = ({ server, model, prefix = '/', config }) => {
const scopes = Object.keys(model.options.scopes);
server.route({
method: 'GET',
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);
},
config: _.defaultsDeep(config, {
validate: {
params: {
scope: joi.string().valid(...scopes),
},
},
}),
});
};
export const create = ({ server, model, prefix = '/', config }) => {
server.route({
method: 'POST',
path: path.join(prefix, model._singular),
@error
async handler(request, reply) {
const instance = await model.create(request.payload);
reply(instance);
},
config,
});
};
export const destroy = ({ server, model, prefix = '/', config }) => {
server.route({
method: 'DELETE',
path: path.join(prefix, model._singular, '{id?}'),
@error
async handler(request, reply) {
const where = parseWhere(request);
if (request.params.id) where[model.primaryKeyField] = request.params.id;
const list = await model.findAll({ where });
await Promise.all(list.map(instance => instance.destroy()));
reply(list.length === 1 ? list[0] : list);
},
config,
});
};
export const destroyAll = ({ server, model, prefix = '/', config }) => {
server.route({
method: 'DELETE',
path: path.join(prefix, model._plural),
@error
async handler(request, reply) {
const where = parseWhere(request);
const list = await model.findAll({ where });
await Promise.all(list.map(instance => instance.destroy()));
reply(list.length === 1 ? list[0] : list);
},
config,
});
};
export const destroyScope = ({ server, model, prefix = '/', config }) => {
const scopes = Object.keys(model.options.scopes);
server.route({
method: 'DELETE',
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);
},
config: _.defaultsDeep(config, {
validate: {
params: {
scope: joi.string().valid(...scopes),
},
},
}),
});
};
export const update = ({ server, model, prefix = '/', config }) => {
server.route({
method: 'PUT',
path: path.join(prefix, model._singular, '{id}'),
@error
async handler(request, reply) {
const { id } = request.params;
const instance = await model.findById(id);
if (!instance) return void reply(notFound(`${id} not found.`));
await instance.update(request.payload);
reply(instance);
},
config: _.defaultsDeep(config, {
validate: {
payload: joi.object().required(),
params: {
id: joi.any(),
},
},
}),
});
};
const methods = {
list, get, scope, create, destroy, destroyAll, destroyScope, update,
};