Merge pull request #23 from mdibaiee/fix-joi-concat

Fix (validation) use joi's concat
This commit is contained in:
Joey Baker 2016-09-08 13:26:50 -07:00 committed by GitHub
commit 469aaec66f
4 changed files with 476 additions and 80 deletions

View File

@ -10,8 +10,8 @@
}, },
"scripts": { "scripts": {
"lint": "eslint src", "lint": "eslint src",
"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": "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='*.test.js' --watch", "tdd": "ava --require babel-register --source='src/**/*.js' --source='!build/**/*' --watch src/**/*.test.js",
"build": "scripty", "build": "scripty",
"watch": "scripty" "watch": "scripty"
}, },
@ -31,6 +31,7 @@
"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.14.0",
"babel-preset-stage-1": "^6.13.0", "babel-preset-stage-1": "^6.13.0",
"babel-register": "^6.14.0",
"eslint": "^3.4.0", "eslint": "^3.4.0",
"eslint-config-pichak": "^1.1.2", "eslint-config-pichak": "^1.1.2",
"eslint-plugin-ava": "^3.0.0", "eslint-plugin-ava": "^3.0.0",

View File

@ -14,6 +14,7 @@ const createAll = ({
config, config,
attributeValidation, attributeValidation,
associationValidation, associationValidation,
scopes,
}) => { }) => {
Object.keys(methods).forEach((method) => { Object.keys(methods).forEach((method) => {
methods[method]({ methods[method]({
@ -25,6 +26,7 @@ const createAll = ({
attributeValidation, attributeValidation,
associationValidation, associationValidation,
config, config,
scopes,
}), }),
}); });
}); });
@ -64,6 +66,8 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi
include: joi.array().items(joi.string().valid(...modelAssociations)), 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 we don't have any permissions set, just create all the methods
if (!permissions) { if (!permissions) {
createAll({ createAll({
@ -73,6 +77,7 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi
config, config,
attributeValidation, attributeValidation,
associationValidation, associationValidation,
scopes,
}); });
// if permissions are set, but we can't parse them, throw an error // if permissions are set, but we can't parse them, throw an error
} else if (!Array.isArray(permissions)) { } else if (!Array.isArray(permissions)) {
@ -87,6 +92,7 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi
config, config,
attributeValidation, attributeValidation,
associationValidation, associationValidation,
scopes,
}); });
// if we've gotten here, we have complex permissions and need to set them // if we've gotten here, we have complex permissions and need to set them
} else { } else {
@ -108,6 +114,7 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi
method, method,
attributeValidation, attributeValidation,
associationValidation, associationValidation,
scopes,
config: permissionConfig, config: permissionConfig,
}), }),
}); });
@ -119,6 +126,7 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi
prefix, prefix,
attributeValidation, attributeValidation,
associationValidation, associationValidation,
scopes,
config: permissionConfig, config: permissionConfig,
}); });
} }
@ -170,19 +178,11 @@ export const get = ({ server, model, prefix = '/', config }) => {
reply(instance.toJSON()); reply(instance.toJSON());
}, },
config: _.defaultsDeep(config, { config,
validate: {
params: {
id: joi.any(),
},
},
}),
}); });
}; };
export const scope = ({ server, model, prefix = '/', config }) => { export const scope = ({ server, model, prefix = '/', config }) => {
const scopes = Object.keys(model.options.scopes);
server.route({ server.route({
method: 'GET', method: 'GET',
path: path.join(prefix, model._plural, '{scope}'), path: path.join(prefix, model._plural, '{scope}'),
@ -198,13 +198,7 @@ export const scope = ({ server, model, prefix = '/', config }) => {
reply(list.map((item) => item.toJSON())); reply(list.map((item) => item.toJSON()));
}, },
config: _.defaultsDeep(config, { config,
validate: {
params: {
scope: joi.string().valid(...scopes),
},
},
}),
}); });
}; };
@ -268,8 +262,6 @@ 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);
server.route({ server.route({
method: 'DELETE', method: 'DELETE',
path: path.join(prefix, model._plural, '{scope}'), path: path.join(prefix, model._plural, '{scope}'),
@ -288,13 +280,7 @@ export const destroyScope = ({ server, model, prefix = '/', config }) => {
const listAsJSON = list.map((item) => item.toJSON()); const listAsJSON = list.map((item) => item.toJSON());
reply(listAsJSON.length === 1 ? listAsJSON[0] : listAsJSON); reply(listAsJSON.length === 1 ? listAsJSON[0] : listAsJSON);
}, },
config: _.defaultsDeep(config, { config,
validate: {
params: {
scope: joi.string().valid(...scopes),
},
},
}),
}); });
}; };
@ -315,14 +301,7 @@ export const update = ({ server, model, prefix = '/', config }) => {
reply(instance.toJSON()); reply(instance.toJSON());
}, },
config: _.defaultsDeep(config, { config,
validate: {
payload: joi.object().required(),
params: {
id: joi.any(),
},
},
}),
}); });
}; };

View File

@ -1,6 +1,15 @@
import { defaultsDeep } from 'lodash'; import { set, get } from 'lodash';
import joi from 'joi'; 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 = { export const sequelizeOperators = {
$and: joi.any(), $and: joi.any(),
$or: joi.any(), $or: joi.any(),
@ -47,41 +56,81 @@ export const payloadMethods = [
'update', 'update',
]; ];
export default ({ method, attributeValidation, associationValidation, config = {} }) => { export const scopeParamsMethods = [
'destroyScope',
'scope',
];
export const idParamsMethods = [
'get',
'update',
];
export default ({
method, attributeValidation, associationValidation, scopes = [], config = {},
}) => {
const hasWhere = whereMethods.includes(method); const hasWhere = whereMethods.includes(method);
const hasInclude = includeMethods.includes(method); const hasInclude = includeMethods.includes(method);
const hasPayload = payloadMethods.includes(method); const hasPayload = payloadMethods.includes(method);
const methodConfig = { ...config }; 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) { if (hasWhere) {
defaultsDeep(methodConfig, { const query = concatToJoiObject(joi.object()
validate: { .keys({
query: {
...attributeValidation, ...attributeValidation,
...sequelizeOperators, ...sequelizeOperators,
}, }),
}, get(methodConfig, 'validate.query')
}); );
methodConfig = set(methodConfig, 'validate.query', query);
} }
if (hasInclude) { if (hasInclude) {
defaultsDeep(methodConfig, { const query = concatToJoiObject(joi.object()
validate: { .keys({
query: {
...associationValidation, ...associationValidation,
}, }),
}, get(methodConfig, 'validate.query')
}); );
methodConfig = set(methodConfig, 'validate.query', query);
} }
if (hasPayload) { if (hasPayload) {
defaultsDeep(methodConfig, { const payload = concatToJoiObject(joi.object()
validate: { .keys({
payload: {
...attributeValidation, ...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; return methodConfig;

View File

@ -5,16 +5,22 @@ import
whereMethods, whereMethods,
includeMethods, includeMethods,
payloadMethods, payloadMethods,
scopeParamsMethods,
idParamsMethods,
sequelizeOperators, sequelizeOperators,
} from './get-config-for-method.js'; } from './get-config-for-method.js';
test.beforeEach((t) => { test.beforeEach((t) => {
t.context.models = ['MyModel'];
t.context.scopes = ['aScope'];
t.context.attributeValidation = { t.context.attributeValidation = {
myKey: joi.any(), myKey: joi.any(),
}; };
t.context.associationValidation = { t.context.associationValidation = {
include: ['MyModel'], include: joi.array().items(joi.string().valid(t.context.models)),
}; };
t.context.config = { t.context.config = {
@ -22,11 +28,10 @@ test.beforeEach((t) => {
}; };
}); });
test('get-config-for-method validate.query seqeulizeOperators', (t) => { test('validate.query seqeulizeOperators', (t) => {
whereMethods.forEach((method) => { whereMethods.forEach((method) => {
const configForMethod = getConfigForMethod({ method }); const configForMethod = getConfigForMethod({ method });
const { query } = configForMethod.validate; const { query } = configForMethod.validate;
const configForMethodValidateQueryKeys = Object.keys(query);
t.truthy( t.truthy(
query, query,
@ -34,15 +39,20 @@ test('get-config-for-method validate.query seqeulizeOperators', (t) => {
); );
Object.keys(sequelizeOperators).forEach((operator) => { Object.keys(sequelizeOperators).forEach((operator) => {
t.ifError(
query.validate({ [operator]: true }).error
, `applies sequelize operator "${operator}" in validate.where for ${method}`
);
});
t.truthy( t.truthy(
configForMethodValidateQueryKeys.includes(operator), query.validate({ notAThing: true }).error
`applies sequelize operator "${operator}" in validate.where for ${method}` , 'errors on a non-valid key'
); );
}); });
}); });
});
test('get-config-for-method validate.query attributeValidation', (t) => { test('validate.query attributeValidation', (t) => {
const { attributeValidation } = t.context; const { attributeValidation } = t.context;
whereMethods.forEach((method) => { whereMethods.forEach((method) => {
@ -50,16 +60,96 @@ test('get-config-for-method validate.query attributeValidation', (t) => {
const { query } = configForMethod.validate; const { query } = configForMethod.validate;
Object.keys(attributeValidation).forEach((key) => { Object.keys(attributeValidation).forEach((key) => {
t.truthy( t.ifError(
query[key] query.validate({ [key]: true }).error
, `applies attributeValidation (${key}) to validate.query` , `applies attributeValidation (${key}) to validate.query`
); );
}); });
t.truthy(
query.validate({ notAThing: true }).error
, 'errors on a non-valid key'
);
}); });
}); });
test('get-config-for-method validate.query associationValidation', (t) => { test('query attributeValidation w/ config as plain object', (t) => {
const { attributeValidation, associationValidation } = t.context; 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) => { includeMethods.forEach((method) => {
const configForMethod = getConfigForMethod({ const configForMethod = getConfigForMethod({
@ -70,22 +160,106 @@ test('get-config-for-method validate.query associationValidation', (t) => {
const { query } = configForMethod.validate; const { query } = configForMethod.validate;
Object.keys(attributeValidation).forEach((key) => { Object.keys(attributeValidation).forEach((key) => {
t.truthy( t.ifError(
query[key] query.validate({ [key]: true }).error
, `applies attributeValidation (${key}) to validate.query when include should be applied` , `applies attributeValidation (${key}) to validate.query when include should be applied`
); );
}); });
Object.keys(associationValidation).forEach((key) => { Object.keys(associationValidation).forEach((key) => {
t.truthy( t.ifError(
query[key] query.validate({ [key]: models }).error
, `applies associationValidation (${key}) to validate.query when include should be applied` , `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('get-config-for-method validate.payload associationValidation', (t) => { 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; const { attributeValidation } = t.context;
payloadMethods.forEach((method) => { payloadMethods.forEach((method) => {
@ -93,20 +267,213 @@ test('get-config-for-method validate.payload associationValidation', (t) => {
const { payload } = configForMethod.validate; const { payload } = configForMethod.validate;
Object.keys(attributeValidation).forEach((key) => { Object.keys(attributeValidation).forEach((key) => {
t.truthy( t.ifError(
payload[key] payload.validate({ [key]: true }).error
, `applies attributeValidation (${key}) to validate.payload` , `applies attributeValidation (${key}) to validate.payload`
); );
}); });
t.truthy(
payload.validate({ notAThing: true }).error
, 'errors on a non-valid key'
);
}); });
}); });
test('get-config-for-method does not modify initial config on multiple passes', (t) => { 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 { config } = t.context;
const originalConfig = { ...config }; const originalConfig = { ...config };
whereMethods.forEach((method) => { whereMethods.forEach((method) => {
getConfigForMethod({ method, config }); getConfigForMethod({ method, ...t.context });
}); });
t.deepEqual( t.deepEqual(