Add feature to allow nested include and filtering relationships/associations #36

Open
labibramadhan wants to merge 8 commits from labibramadhan/include-with-filter into master
4 changed files with 63 additions and 39 deletions
Showing only changes of commit 72452a0088 - Show all commits

View File

@ -85,3 +85,13 @@ test('multiple includes /team?include[]=players&include[]=city', async (t) => {
t.truthy(playerIds.includes(player2.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);
});

View File

@ -13,7 +13,7 @@ const createAll = ({
prefix,
config,
attributeValidation,
associationValidation,
modelAssociations,
scopes,
}) => {
Object.keys(methods).forEach((method) => {
@ -24,7 +24,7 @@ const createAll = ({
config: getConfigForMethod({
method,
attributeValidation,
associationValidation,
modelAssociations,
config,
scopes,
}),
@ -35,22 +35,22 @@ const createAll = ({
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: {
cat: ['list', 'get']
, dog: true // all
}
```
```
models: {
cat: ['list', 'get']
, dog: true // all
}
```
*/
*/
export default (server, model, { prefix, defaultConfig: config, models: permissions }) => {
const modelName = model._singular;
@ -71,13 +71,6 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi
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);
// 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,
config,
attributeValidation,
associationValidation,
modelAssociations,
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)) {
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
// 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,
@ -103,10 +96,10 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi
prefix,
config,
attributeValidation,
associationValidation,
modelAssociations,
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 {
const permissionOptions = permissions.filter((permission) => {
return permission.model === modelName;
@ -125,7 +118,7 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi
config: getConfigForMethod({
method,
attributeValidation,
associationValidation,
modelAssociations,
scopes,
config: permissionConfig,
}),
@ -137,7 +130,7 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi
model,
prefix,
attributeValidation,
associationValidation,
modelAssociations,
scopes,
config: permissionConfig,
});
@ -257,7 +250,7 @@ export const destroy = ({ server, model, prefix = '/', config }) => {
return void reply(id
? notFound(`${id} not found.`)
: notFound('Nothing found.')
);
);
}
await Promise.all(list.map(instance => instance.destroy()));
@ -286,7 +279,7 @@ export const destroyAll = ({ server, model, prefix = '/', config }) => {
return void reply(id
? notFound(`${id} not found.`)
: notFound('Nothing found.')
);
);
}
await Promise.all(list.map(instance => instance.destroy()));

View File

@ -72,7 +72,7 @@ export const restrictMethods = [
];
export default ({
method, attributeValidation, associationValidation, scopes = [], config = {},
method, attributeValidation, modelAssociations, scopes = [], config = {},
}) => {
const hasWhere = whereMethods.includes(method);
const hasInclude = includeMethods.includes(method);
@ -96,9 +96,27 @@ export default ({
}
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()
.keys({
...associationValidation,
include: [
joi.array().items(validAssociationsString),
joi.array().items(validAssociationsObject),
validAssociationsString,
validAssociationsObject,
],
}),
get(methodConfig, 'validate.query')
);

View File

@ -25,13 +25,16 @@ export const parseInclude = request => {
const models = getModels(request);
labibramadhan commented 2016-11-08 01:29:34 +00:00 (Migrated from github.com)
Review

maybe like this @joeybaker?

maybe like this @joeybaker?
joeybaker commented 2016-11-08 17:16:04 +00:00 (Migrated from github.com)
Review

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, because joi.string() always returns a truthy value.

What do you think of something like this in crud.js on line 77?

  const assocationJSONvaliation = joi.string().regex(/^\{.*?"model":.*?\}$/);
  const associationValidation = {
    include: [
      joi.array().items(validAssociations),
      joi.array().items(assocationJSONvaliation),
      validAssociations,
      assocationJSONvaliation,
    ],
  };
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, because `joi.string()` always returns a truthy value. What do you think of something like this in `crud.js` on line 77? ``` js const assocationJSONvaliation = joi.string().regex(/^\{.*?"model":.*?\}$/); const associationValidation = { include: [ joi.array().items(validAssociations), joi.array().items(assocationJSONvaliation), validAssociations, assocationJSONvaliation, ], }; ```
labibramadhan commented 2016-11-09 01:48:34 +00:00 (Migrated from github.com)
Review

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)

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)`
labibramadhan commented 2016-11-09 01:52:20 +00:00 (Migrated from github.com)
Review

currently my version of crud.js has this to validate its association object:

joi.object().keys({
        model: joi.string().valid(...modelAssociations),
        where: joi.object().keys({
          ...attributeValidation,
          ...sequelizeOperators,
        }),
      })
currently my version of crud.js has this to validate its association object: ``` joi.object().keys({ model: joi.string().valid(...modelAssociations), where: joi.object().keys({ ...attributeValidation, ...sequelizeOperators, }), }) ```
joeybaker commented 2016-11-09 05:46:48 +00:00 (Migrated from github.com)
Review

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:

joi.object().keys({
        model: joi.string().valid(...modelAssociations),
        where: joi.object().keys({
          ...attributeValidation,
          ...sequelizeOperators,
        }),
      })

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?

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: ``` js joi.object().keys({ model: joi.string().valid(...modelAssociations), where: joi.object().keys({ ...attributeValidation, ...sequelizeOperators, }), }) ``` 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?
labibramadhan commented 2016-11-09 19:01:15 +00:00 (Migrated from github.com)
Review

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

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
if (models.isBoom) return models;
return include.map(a => {
const singluarOrPluralMatch = Object.keys(models).find((modelName) => {
const { _singular, _plural } = models[modelName];
return _singular === a || _plural === a;
});
return include.map(b => {
const a = /^{.*}$/.test(b) ? JSON.parse(b) : b;
if (typeof a !== 'object') {
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];