Add feature to allow nested include and filtering relationships/associations #36
50
README.md
50
README.md
@ -112,9 +112,14 @@ Getting related models is easy, just use a query parameter `include`.
|
||||
// GET /teams?include=city or
|
||||
// GET /teams?include={"model": "City"}
|
||||
|
||||
|
||||
// results in a Sequelize query:
|
||||
Team.findAll({include: City})
|
||||
|
||||
or if association defined with an alias
|
||||
// GET /players?include={"model": "Master", "as": "Couch"}
|
||||
|
||||
// results in a Sequelize query:
|
||||
Players.findAll({include: Master, as: 'Couch'})
|
||||
|
||||
```
|
||||
|
||||
If you want to get multiple related models, just pass multiple `include` parameters.
|
||||
@ -135,7 +140,7 @@ For models that have a many-to-many relationship, you can also pass the plural v
|
||||
Team.findAll({include: [Player]})
|
||||
```
|
||||
|
||||
Filtering by related models property, you can pass **where** paremeter inside each **include** item(s) object.
|
||||
Filtering by related models property, you can pass **where** paremeter inside each **include** items object.
|
||||
```js
|
||||
// returns all team with their related City where City property name equals Healdsburg
|
||||
// GET /teams?include={"model": "City", "where": {"name": "Healdsburg"}}
|
||||
@ -144,6 +149,47 @@ Filtering by related models property, you can pass **where** paremeter inside ea
|
||||
Team.findAll({include: {model: City, where: {name: 'Healdsburg'}}})
|
||||
```
|
||||
|
||||
More complex example with nested include, association alias and association filtering.
|
||||
```js
|
||||
// returns all team with its players along with its couch of each player
|
||||
// GET /cities?include[]={
|
||||
// "model": "Team",
|
||||
// "include": {
|
||||
// "model": "Player",
|
||||
// "where": {
|
||||
// "name": "Pinot"
|
||||
// },
|
||||
// "include": {
|
||||
// "model": "Master",
|
||||
// "as": "Coach",
|
||||
// "where": {
|
||||
// "name": "Shifu"
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// results in a Sequelize query:
|
||||
City.findAll({
|
||||
include: {
|
||||
model: Team,
|
||||
include: {
|
||||
model: Player,
|
||||
where: {
|
||||
name: Pinot
|
||||
},
|
||||
include: {
|
||||
model: Master,
|
||||
as: 'Coach',
|
||||
where: {
|
||||
name: 'Shifu'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## `limit` and `offset` queries
|
||||
Restricting list (`GET`) and scope queries to a restricted count can be done by passing `limit=<number>` and/or `offset=<number>`.
|
||||
|
||||
|
@ -112,7 +112,7 @@ test('multiple includes /team?include[]=players&include[]={"model": "City"}', as
|
||||
t.is(result.City.id, city1.id);
|
||||
});
|
||||
|
||||
test('inlcude filter /teams?include[]={"model": "City", "where": {"name": "Healdsburg"}}'
|
||||
test('include filter /teams?include[]={"model": "City", "where": {"name": "Healdsburg"}}'
|
||||
, async(t) => {
|
||||
const { server } = t.context;
|
||||
const url = '/teams?include[]={"model": "City", "where": {"name": "Healdsburg"}}';
|
||||
@ -122,29 +122,14 @@ test('inlcude filter /teams?include[]={"model": "City", "where": {"name": "Heald
|
||||
t.is(statusCode, STATUS_OK);
|
||||
});
|
||||
|
||||
test('nested inlcude filter ' +
|
||||
'/city?include[]={"model": "Team", "include": {"model": "Player", "where": {"name": "Pinot"}}}'
|
||||
, async(t) => {
|
||||
const { instances, server } = t.context;
|
||||
const { city1, team1, player2 } = instances;
|
||||
// eslint-disable-next-line max-len
|
||||
const url = '/city?include[]={"model": "Team", "include": {"model": "Player", "where": {"name": "Pinot"}}}';
|
||||
const method = 'GET';
|
||||
|
||||
const { result, statusCode } = await server.inject({ url, method });
|
||||
t.is(statusCode, STATUS_OK);
|
||||
t.is(result.id, city1.id);
|
||||
t.is(result.Teams[0].id, team1.id);
|
||||
t.is(result.Teams[0].Players[0].id, player2.id);
|
||||
});
|
||||
|
||||
test('nested inlcude filter ' +
|
||||
'/city?include[]={"model": "Team", "include": {"model": "City", "where": {"name": "Healdsburg"}}}'
|
||||
test('nested include filter ' +
|
||||
'/citiy?include[]=' +
|
||||
'{"model": "Team", "include": {"model": "City", "where": {"name": "Healdsburg"}}}'
|
||||
, async(t) => {
|
||||
const { instances, server } = t.context;
|
||||
const { city1, team1, team2 } = instances;
|
||||
// eslint-disable-next-line max-len
|
||||
const url = '/city?include[]={"model": "Team", "include": {"model": "City", "where": {"name": "Healdsburg"}}}';
|
||||
const url = '/city?include[]=' +
|
||||
'{"model": "Team", "include": {"model": "City", "where": {"name": "Healdsburg"}}}';
|
||||
const method = 'GET';
|
||||
|
||||
const { result, statusCode } = await server.inject({ url, method });
|
||||
@ -155,3 +140,23 @@ test('nested inlcude filter ' +
|
||||
t.truthy(teamIds.includes(team1.id));
|
||||
t.truthy(teamIds.includes(team2.id));
|
||||
});
|
||||
|
||||
test('complex include ' +
|
||||
'/cities?include[]={"model":"Team", ' +
|
||||
'"include":{ "model":"Player", "where":{"name": "Pinot"}, ' +
|
||||
'"include":{ "model":"Master", "as":"Coach", "where":{"name": "Shifu"}}}}'
|
||||
, async(t) => {
|
||||
const { instances, server } = t.context;
|
||||
const { city1, master1, player2, team1 } = instances;
|
||||
const method = 'GET';
|
||||
const url = '/cities?include[]={"model":"Team", ' +
|
||||
'"include":{ "model":"Player", "where":{"name": "Pinot"}, ' +
|
||||
'"include":{ "model":"Master", "as":"Coach", "where":{"name": "Shifu"}}}}';
|
||||
|
||||
const { result, statusCode } = await server.inject({ url, method });
|
||||
t.is(statusCode, STATUS_OK);
|
||||
t.is(result[0].id, city1.id);
|
||||
t.is(result[0].Teams[0].id, team1.id);
|
||||
t.is(result[0].Teams[0].Players[0].id, player2.id);
|
||||
t.is(result[0].Teams[0].Players[0].Coach.id, master1.id);
|
||||
});
|
||||
|
31
src/utils.js
31
src/utils.js
@ -18,18 +18,7 @@ const getModels = (request) => {
|
||||
return models;
|
||||
};
|
||||
|
||||
export const parseInclude = async(request) => {
|
||||
if (typeof request.query.include === 'undefined') return [];
|
||||
|
||||
const include = Array.isArray(request.query.include)
|
||||
? request.query.include
|
||||
: [request.query.include]
|
||||
;
|
||||
|
||||
const models = getModels(request);
|
||||
if (models.isBoom) return models;
|
||||
|
||||
const getModelInstance = includeItem => {
|
||||
const getModelInstance = (models, includeItem) => {
|
||||
return new Promise(async(resolve) => {
|
||||
if (includeItem) {
|
||||
if (typeof includeItem !== 'object') {
|
||||
`JSON.parse` will throw if not handed valid JSON. We should use a try/catch here.
what code inside the catch do you prefer? what code inside the catch do you prefer?
https://github.com/mdibaiee/hapi-sequelize-crud/blob/5ba9d7d26100f1a1a82367097342a8c35d43a26f/src/utils.js#L50-L54 and https://github.com/mdibaiee/hapi-sequelize-crud/blob/5ba9d7d26100f1a1a82367097342a8c35d43a26f/src/utils.js#L78-L83 are both options. Thanks!
|
||||
@ -54,18 +43,28 @@ export const parseInclude = async(request) => {
|
||||
includeItem.model = models[includeItem.model];
|
||||
}
|
||||
if (includeItem.hasOwnProperty('include')) {
|
||||
includeItem.include = await getModelInstance(includeItem.include);
|
||||
includeItem.include = await getModelInstance(models, includeItem.include);
|
||||
return resolve(includeItem);
|
||||
} else {
|
||||
return resolve(includeItem);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return resolve(includeItem);
|
||||
}
|
||||
return resolve(includeItem);
|
||||
});
|
||||
};
|
||||
|
||||
export const parseInclude = async(request) => {
|
||||
if (typeof request.query.include === 'undefined') return [];
|
||||
|
||||
const include = Array.isArray(request.query.include)
|
||||
? request.query.include
|
||||
: [request.query.include]
|
||||
;
|
||||
|
||||
const models = getModels(request);
|
||||
if (models.isBoom) return models;
|
||||
|
||||
const jsonValidation = joi.string().regex(/^\{.*?"model":.*?\}$/);
|
||||
const includes = include.map(async(b) => {
|
||||
let a = b;
|
||||
@ -77,7 +76,7 @@ export const parseInclude = async(request) => {
|
||||
//
|
||||
}
|
||||
|
||||
return getModelInstance(a);
|
||||
return getModelInstance(models, a);
|
||||
}).filter(identity);
|
||||
|
||||
return await Promise.all(includes);
|
||||
|
Loading…
Reference in New Issue
Block a user
Please get rid of the
include
key of the JSON passed toinclude
, it's an unnecessary repetition.It should look like this:
/teams?include={"model": "City", "where": { "name": "Healdsburg" }}
Fixed in my last commit, could you verify now?