Add feature to allow nested include and filtering relationships/associations #36
@ -85,3 +85,13 @@ test('multiple includes /team?include[]=players&include[]=city', async (t) => {
|
|||||||
t.truthy(playerIds.includes(player2.id));
|
t.truthy(playerIds.includes(player2.id));
|
||||||
t.is(result.City.id, city1.id);
|
t.is(result.City.id, city1.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('inlcude filter /teams?include[]={"model": "City", "where": {"name": "Healdsburg"}}'
|
||||||
|
, async (t) => {
|
||||||
|
const { server } = t.context;
|
||||||
|
const url = '/teams?include[]={"model": "City", "where": {"name": "Healdsburg"}}';
|
||||||
|
const method = 'GET';
|
||||||
|
|
||||||
|
const { statusCode } = await server.inject({ url, method });
|
||||||
|
t.is(statusCode, STATUS_OK);
|
||||||
|
});
|
55
src/crud.js
55
src/crud.js
@ -13,7 +13,7 @@ const createAll = ({
|
|||||||
prefix,
|
prefix,
|
||||||
config,
|
config,
|
||||||
attributeValidation,
|
attributeValidation,
|
||||||
associationValidation,
|
modelAssociations,
|
||||||
scopes,
|
scopes,
|
||||||
}) => {
|
}) => {
|
||||||
Object.keys(methods).forEach((method) => {
|
Object.keys(methods).forEach((method) => {
|
||||||
@ -24,7 +24,7 @@ const createAll = ({
|
|||||||
config: getConfigForMethod({
|
config: getConfigForMethod({
|
||||||
method,
|
method,
|
||||||
attributeValidation,
|
attributeValidation,
|
||||||
associationValidation,
|
modelAssociations,
|
||||||
config,
|
config,
|
||||||
scopes,
|
scopes,
|
||||||
}),
|
}),
|
||||||
@ -35,22 +35,22 @@ const createAll = ({
|
|||||||
export { associations };
|
export { associations };
|
||||||
|
|
||||||
/*
|
/*
|
||||||
The `models` option, becomes `permissions`, and can look like:
|
The `models` option, becomes `permissions`, and can look like:
|
||||||
|
|
||||||
```
|
```
|
||||||
models: ['cat', 'dog']
|
models: ['cat', 'dog']
|
||||||
```
|
```
|
||||||
|
|
||||||
or
|
or
|
||||||
|
|
||||||
```
|
```
|
||||||
models: {
|
models: {
|
||||||
cat: ['list', 'get']
|
cat: ['list', 'get']
|
||||||
, dog: true // all
|
, dog: true // all
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
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;
|
||||||
@ -71,13 +71,6 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi
|
|||||||
return params;
|
return params;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
const validAssociations = modelAssociations.length
|
|
||||||
? joi.string().valid(...modelAssociations)
|
|
||||||
: joi.valid(null);
|
|
||||||
const associationValidation = {
|
|
||||||
include: [joi.array().items(validAssociations), validAssociations],
|
|
||||||
};
|
|
||||||
|
|
||||||
const scopes = Object.keys(model.options.scopes);
|
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
|
||||||
@ -88,14 +81,14 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi
|
|||||||
prefix,
|
prefix,
|
||||||
config,
|
config,
|
||||||
attributeValidation,
|
attributeValidation,
|
||||||
associationValidation,
|
modelAssociations,
|
||||||
scopes,
|
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)) {
|
||||||
throw new Error('hapi-sequelize-crud: `models` property must be an array');
|
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
|
// 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
|
// are no permissions to be set, so just create all methods and move on
|
||||||
} else if (permissions.includes(modelName)) {
|
} else if (permissions.includes(modelName)) {
|
||||||
createAll({
|
createAll({
|
||||||
server,
|
server,
|
||||||
@ -103,10 +96,10 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi
|
|||||||
prefix,
|
prefix,
|
||||||
config,
|
config,
|
||||||
attributeValidation,
|
attributeValidation,
|
||||||
associationValidation,
|
modelAssociations,
|
||||||
scopes,
|
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 {
|
||||||
const permissionOptions = permissions.filter((permission) => {
|
const permissionOptions = permissions.filter((permission) => {
|
||||||
return permission.model === modelName;
|
return permission.model === modelName;
|
||||||
@ -125,7 +118,7 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi
|
|||||||
config: getConfigForMethod({
|
config: getConfigForMethod({
|
||||||
method,
|
method,
|
||||||
attributeValidation,
|
attributeValidation,
|
||||||
associationValidation,
|
modelAssociations,
|
||||||
scopes,
|
scopes,
|
||||||
config: permissionConfig,
|
config: permissionConfig,
|
||||||
}),
|
}),
|
||||||
@ -137,7 +130,7 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi
|
|||||||
model,
|
model,
|
||||||
prefix,
|
prefix,
|
||||||
attributeValidation,
|
attributeValidation,
|
||||||
associationValidation,
|
modelAssociations,
|
||||||
scopes,
|
scopes,
|
||||||
config: permissionConfig,
|
config: permissionConfig,
|
||||||
});
|
});
|
||||||
@ -257,7 +250,7 @@ export const destroy = ({ server, model, prefix = '/', config }) => {
|
|||||||
return void reply(id
|
return void reply(id
|
||||||
? notFound(`${id} not found.`)
|
? notFound(`${id} not found.`)
|
||||||
: notFound('Nothing found.')
|
: notFound('Nothing found.')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(list.map(instance => instance.destroy()));
|
await Promise.all(list.map(instance => instance.destroy()));
|
||||||
@ -286,7 +279,7 @@ export const destroyAll = ({ server, model, prefix = '/', config }) => {
|
|||||||
return void reply(id
|
return void reply(id
|
||||||
? notFound(`${id} not found.`)
|
? notFound(`${id} not found.`)
|
||||||
: notFound('Nothing found.')
|
: notFound('Nothing found.')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(list.map(instance => instance.destroy()));
|
await Promise.all(list.map(instance => instance.destroy()));
|
||||||
|
@ -72,7 +72,7 @@ export const restrictMethods = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export default ({
|
export default ({
|
||||||
method, attributeValidation, associationValidation, scopes = [], config = {},
|
method, attributeValidation, modelAssociations, scopes = [], config = {},
|
||||||
}) => {
|
}) => {
|
||||||
const hasWhere = whereMethods.includes(method);
|
const hasWhere = whereMethods.includes(method);
|
||||||
const hasInclude = includeMethods.includes(method);
|
const hasInclude = includeMethods.includes(method);
|
||||||
@ -96,9 +96,27 @@ export default ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasInclude) {
|
if (hasInclude) {
|
||||||
|
const modelsHasAssociations = modelAssociations && modelAssociations.length;
|
||||||
|
const validAssociationsString = modelsHasAssociations
|
||||||
|
? joi.string().valid(...modelAssociations)
|
||||||
|
: joi.valid(null);
|
||||||
|
const validAssociationsObject = modelsHasAssociations
|
||||||
|
? joi.object().keys({
|
||||||
|
model: joi.string().valid(...modelAssociations),
|
||||||
|
where: joi.object().keys({
|
||||||
|
...attributeValidation,
|
||||||
|
...sequelizeOperators,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
: joi.valid(null);
|
||||||
const query = concatToJoiObject(joi.object()
|
const query = concatToJoiObject(joi.object()
|
||||||
.keys({
|
.keys({
|
||||||
...associationValidation,
|
include: [
|
||||||
|
joi.array().items(validAssociationsString),
|
||||||
|
joi.array().items(validAssociationsObject),
|
||||||
|
validAssociationsString,
|
||||||
|
validAssociationsObject,
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
get(methodConfig, 'validate.query')
|
get(methodConfig, 'validate.query')
|
||||||
);
|
);
|
||||||
|
15
src/utils.js
15
src/utils.js
@ -25,13 +25,16 @@ export const parseInclude = request => {
|
|||||||
const models = getModels(request);
|
const models = getModels(request);
|
||||||
|
|||||||
if (models.isBoom) return models;
|
if (models.isBoom) return models;
|
||||||
|
|
||||||
return include.map(a => {
|
return include.map(b => {
|
||||||
const singluarOrPluralMatch = Object.keys(models).find((modelName) => {
|
const a = /^{.*}$/.test(b) ? JSON.parse(b) : b;
|
||||||
const { _singular, _plural } = models[modelName];
|
if (typeof a !== 'object') {
|
||||||
return _singular === a || _plural === a;
|
const singluarOrPluralMatch = Object.keys(models).find((modelName) => {
|
||||||
});
|
const { _singular, _plural } = models[modelName];
|
||||||
|
return _singular === a || _plural === a;
|
||||||
|
});
|
||||||
|
|
||||||
if (singluarOrPluralMatch) return models[singluarOrPluralMatch];
|
if (singluarOrPluralMatch) return models[singluarOrPluralMatch];
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof a === 'string') return models[a];
|
if (typeof a === 'string') return models[a];
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user
maybe like this @joeybaker?
Kinda! The try/catch part is right, but the joi check should be a part of the validation. What you have here will always pass the
if
condition, becausejoi.string()
always returns a truthy value.What do you think of something like this in
crud.js
on line 77?but with this code, it won't validate the "model" name and the "where" object isn't it?
if the JSON string doesn't parse good so it caught as a string, but later it won't pass because crud.js that i pushed has something like
joi.string().valid(...modelAssociations)
currently my version of crud.js has this to validate its association object:
I get it. I was opting for: "if you pass JSON, we're going to skip validating it … it's too complex to do correctly".
Here's the problem I see with:
It ensures you get a valid JS object to validate. But you won't. You'll get a string that contains JSON. That's why I was opting for the regex of a string. Did I get confused there?
sorry @joeybaker, but i still didn't get it, what the main purpose of adding the json validation you want? because i have tried several cases of using include parameter and i didn't find any include parameter validation problem