From 05793eb7499a1393058a2e35b2c46aefa3994345 Mon Sep 17 00:00:00 2001 From: Muhammad Labib Ramadhan Date: Fri, 11 Nov 2016 09:03:01 +0700 Subject: [PATCH] Moved "getModelInstance" function to the outside "parseInclude" function and added README and an "include" test about complex include feature --- README.md | 50 ++++++++++++++++++- src/crud-include.integration.test.js | 47 +++++++++-------- src/utils.js | 75 ++++++++++++++-------------- 3 files changed, 111 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 70a55b1..60892fc 100644 --- a/README.md +++ b/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=` and/or `offset=`. diff --git a/src/crud-include.integration.test.js b/src/crud-include.integration.test.js index cefc774..df342d2 100644 --- a/src/crud-include.integration.test.js +++ b/src/crud-include.integration.test.js @@ -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); +}); diff --git a/src/utils.js b/src/utils.js index 4ea6481..db69992 100644 --- a/src/utils.js +++ b/src/utils.js @@ -18,6 +18,42 @@ const getModels = (request) => { return models; }; +const getModelInstance = (models, includeItem) => { + return new Promise(async(resolve) => { + if (includeItem) { + if (typeof includeItem !== 'object') { + const singluarOrPluralMatch = Object.keys(models).find((modelName) => { + const { _singular, _plural } = models[modelName]; + return _singular === includeItem || _plural === includeItem; + }); + + if (singluarOrPluralMatch) { + return resolve(models[singluarOrPluralMatch]); + } + } + + if (typeof includeItem === 'string' && models.hasOwnProperty(includeItem)) { + return resolve(models[includeItem]); + } else if (typeof includeItem === 'object') { + if ( + typeof includeItem.model === 'string' && + includeItem.model.length && + models.hasOwnProperty(includeItem.model) + ) { + includeItem.model = models[includeItem.model]; + } + if (includeItem.hasOwnProperty('include')) { + includeItem.include = await getModelInstance(models, includeItem.include); + return resolve(includeItem); + } else { + return resolve(includeItem); + } + } + } + return resolve(includeItem); + }); +}; + export const parseInclude = async(request) => { if (typeof request.query.include === 'undefined') return []; @@ -29,43 +65,6 @@ export const parseInclude = async(request) => { const models = getModels(request); if (models.isBoom) return models; - const getModelInstance = includeItem => { - return new Promise(async(resolve) => { - if (includeItem) { - if (typeof includeItem !== 'object') { - const singluarOrPluralMatch = Object.keys(models).find((modelName) => { - const { _singular, _plural } = models[modelName]; - return _singular === includeItem || _plural === includeItem; - }); - - if (singluarOrPluralMatch) { - return resolve(models[singluarOrPluralMatch]); - } - } - - if (typeof includeItem === 'string' && models.hasOwnProperty(includeItem)) { - return resolve(models[includeItem]); - } else if (typeof includeItem === 'object') { - if ( - typeof includeItem.model === 'string' && - includeItem.model.length && - models.hasOwnProperty(includeItem.model) - ) { - includeItem.model = models[includeItem.model]; - } - if (includeItem.hasOwnProperty('include')) { - includeItem.include = await getModelInstance(includeItem.include); - return resolve(includeItem); - } else { - return resolve(includeItem); - } - } - } else { - return resolve(includeItem); - } - }); - }; - 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);