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
414 lines
9.1 KiB
JavaScript
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,
|
|
};
|