From 72452a0088c8af147b640f884742a9b0c00f1740 Mon Sep 17 00:00:00 2001 From: Muhammad Labib Ramadhan Date: Fri, 4 Nov 2016 11:31:56 +0700 Subject: [PATCH 1/8] Added feature to allow filtering relationships/associations based on http://docs.sequelizejs.com/en/latest/docs/querying/#relations-associations --- src/crud-include.integration.test.js | 10 +++++ src/crud.js | 55 ++++++++++++---------------- src/get-config-for-method.js | 22 ++++++++++- src/utils.js | 15 +++++--- 4 files changed, 63 insertions(+), 39 deletions(-) diff --git a/src/crud-include.integration.test.js b/src/crud-include.integration.test.js index 169781f..797d89a 100644 --- a/src/crud-include.integration.test.js +++ b/src/crud-include.integration.test.js @@ -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); + }); \ No newline at end of file diff --git a/src/crud.js b/src/crud.js index be5cc6a..d7bbc7f 100644 --- a/src/crud.js +++ b/src/crud.js @@ -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())); diff --git a/src/get-config-for-method.js b/src/get-config-for-method.js index d6bc661..0c3a28b 100644 --- a/src/get-config-for-method.js +++ b/src/get-config-for-method.js @@ -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') ); diff --git a/src/utils.js b/src/utils.js index 6f547b5..ad80096 100644 --- a/src/utils.js +++ b/src/utils.js @@ -25,13 +25,16 @@ export const parseInclude = request => { const models = getModels(request); 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]; -- 2.34.1 From a141a38fe54d7d59eeaebd275195f6fcf0125101 Mon Sep 17 00:00:00 2001 From: Muhammad Labib Ramadhan Date: Fri, 4 Nov 2016 14:16:30 +0700 Subject: [PATCH 2/8] Fixed getConfig test error about association validation --- src/get-config-for-method.test.js | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/get-config-for-method.test.js b/src/get-config-for-method.test.js index ea8edad..739bf8d 100644 --- a/src/get-config-for-method.test.js +++ b/src/get-config-for-method.test.js @@ -20,8 +20,19 @@ test.beforeEach((t) => { myKey: joi.any(), }; + const validAssociationsString = joi.string().valid(...t.context.models); + const validAssociationsObject = joi.object().keys({ + model: joi.string().valid(...t.context.models), + where: joi.object(), + }); + t.context.associationValidation = { - include: joi.array().items(joi.string().valid(t.context.models)), + include: [ + joi.array().items(validAssociationsString), + joi.array().items(validAssociationsObject), + validAssociationsString, + validAssociationsObject, + ], }; t.context.config = { @@ -151,12 +162,12 @@ test('query attributeValidation w/ config as joi object', (t) => { test('validate.query associationValidation', (t) => { const { attributeValidation, associationValidation, models } = t.context; - + const modelAssociations = models; includeMethods.forEach((method) => { const configForMethod = getConfigForMethod({ method, attributeValidation, - associationValidation, + modelAssociations, }); const { query } = configForMethod.validate; @@ -183,6 +194,7 @@ test('validate.query associationValidation', (t) => { test('query associationValidation w/ config as plain object', (t) => { const { associationValidation, models } = t.context; + const modelAssociations = models; const config = { validate: { query: { @@ -194,7 +206,7 @@ test('query associationValidation w/ config as plain object', (t) => { includeMethods.forEach((method) => { const configForMethod = getConfigForMethod({ method, - associationValidation, + modelAssociations, config, }); const { query } = configForMethod.validate; @@ -222,6 +234,7 @@ test('query associationValidation w/ config as plain object', (t) => { test('query associationValidation w/ config as joi object', (t) => { const { associationValidation, models } = t.context; + const modelAssociations = models; const queryKeys = { aKey: joi.boolean(), }; @@ -234,7 +247,7 @@ test('query associationValidation w/ config as joi object', (t) => { includeMethods.forEach((method) => { const configForMethod = getConfigForMethod({ method, - associationValidation, + modelAssociations, config, }); const { query } = configForMethod.validate; -- 2.34.1 From 1977304287d7c058ab977b35727a36fd9c12a240 Mon Sep 17 00:00:00 2001 From: Muhammad Labib Ramadhan Date: Fri, 4 Nov 2016 14:24:01 +0700 Subject: [PATCH 3/8] Added README about association filtering --- README.md | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index bf52e64..0972307 100644 --- a/README.md +++ b/README.md @@ -92,24 +92,14 @@ It's easy to restrict your requests using Sequelize's `where` query option. Just Team.findOne({ where: { city: 'windsor' }}) ``` -You can also do more complex queries by setting the value of a key to JSON. - -```js -// returns only teams that have a `address.city` property of "windsor" -// GET /team?city={"address": "windsor"} -// or -// GET /team?city[address]=windsor - -// results in the Sequelize query: -Team.findOne({ where: { address: { city: 'windsor' }}}) -``` - ## `include` queries Getting related models is easy, just use a query parameter `include`. ```js // returns all teams with their related City model -// GET /teams?include=city +// GET /teams?include=city or +// GET /teams?include={"include": {"model": "City"}}} + // results in a Sequelize query: Team.findAll({include: City}) @@ -133,6 +123,15 @@ 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. +```js +// returns all team with their related City where City property name equals Healdsburg +// GET /teams?include={"include": {"model": "City", "where": {"name": "Healdsburg"}}} + +// results in a Sequelize query: +Team.findAll({include: {model: City, where: {name: 'Healdsburg'}}}) +``` + ## `limit` and `offset` queries Restricting list (`GET`) and scope queries to a restricted count can be done by passing `limit=` and/or `offset=`. -- 2.34.1 From 11306667d66fc1b05f036c0e4fcc975da76f3d09 Mon Sep 17 00:00:00 2001 From: Muhammad Labib Ramadhan Date: Mon, 7 Nov 2016 08:48:52 +0700 Subject: [PATCH 4/8] Refactored codes to not modify get-config-for-method.test.js, moved validationAssociations definition logic from get-config-for-method.js to crud.js, fixed README.md --- README.md | 16 +++++++-- src/crud.js | 55 +++++++++++++++++++++---------- src/get-config-for-method.js | 22 ++----------- src/get-config-for-method.test.js | 23 +++---------- 4 files changed, 59 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 0972307..70a55b1 100644 --- a/README.md +++ b/README.md @@ -92,13 +92,25 @@ It's easy to restrict your requests using Sequelize's `where` query option. Just Team.findOne({ where: { city: 'windsor' }}) ``` +You can also do more complex queries by setting the value of a key to JSON. + +```js +// returns only teams that have a `address.city` property of "windsor" +// GET /team?city={"address": "windsor"} +// or +// GET /team?city[address]=windsor + +// results in the Sequelize query: +Team.findOne({ where: { address: { city: 'windsor' }}}) +``` + ## `include` queries Getting related models is easy, just use a query parameter `include`. ```js // returns all teams with their related City model // GET /teams?include=city or -// GET /teams?include={"include": {"model": "City"}}} +// GET /teams?include={"model": "City"} // results in a Sequelize query: @@ -126,7 +138,7 @@ Team.findAll({include: [Player]}) Filtering by related models property, you can pass **where** paremeter inside each **include** item(s) object. ```js // returns all team with their related City where City property name equals Healdsburg -// GET /teams?include={"include": {"model": "City", "where": {"name": "Healdsburg"}}} +// GET /teams?include={"model": "City", "where": {"name": "Healdsburg"}} // results in a Sequelize query: Team.findAll({include: {model: City, where: {name: 'Healdsburg'}}}) diff --git a/src/crud.js b/src/crud.js index d7bbc7f..3037a2e 100644 --- a/src/crud.js +++ b/src/crud.js @@ -5,16 +5,16 @@ import _ from 'lodash'; import { parseInclude, parseWhere, parseLimitAndOffset, parseOrder } from './utils'; import { notFound } from 'boom'; import * as associations from './associations/index'; -import getConfigForMethod from './get-config-for-method.js'; +import getConfigForMethod, { sequelizeOperators } from './get-config-for-method.js'; const createAll = ({ - server, - model, - prefix, - config, - attributeValidation, - modelAssociations, - scopes, + server, + model, + prefix, + config, + attributeValidation, + associationValidation, + scopes, }) => { Object.keys(methods).forEach((method) => { methods[method]({ @@ -24,7 +24,7 @@ const createAll = ({ config: getConfigForMethod({ method, attributeValidation, - modelAssociations, + associationValidation, config, scopes, }), @@ -71,6 +71,27 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi return params; }, {}); + 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 associationValidation = { + include: [ + joi.array().items(validAssociationsString), + joi.array().items(validAssociationsObject), + validAssociationsString, + validAssociationsObject, + ], + }; const scopes = Object.keys(model.options.scopes); // if we don't have any permissions set, just create all the methods @@ -81,7 +102,7 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi prefix, config, attributeValidation, - modelAssociations, + associationValidation, scopes, }); // if permissions are set, but we can't parse them, throw an error @@ -96,7 +117,7 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi prefix, config, attributeValidation, - modelAssociations, + associationValidation, scopes, }); // if we've gotten here, we have complex permissions and need to set them @@ -118,7 +139,7 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi config: getConfigForMethod({ method, attributeValidation, - modelAssociations, + associationValidation, scopes, config: permissionConfig, }), @@ -130,7 +151,7 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi model, prefix, attributeValidation, - modelAssociations, + associationValidation, scopes, config: permissionConfig, }); @@ -248,8 +269,8 @@ export const destroy = ({ server, model, prefix = '/', config }) => { if (!list.length) { return void reply(id - ? notFound(`${id} not found.`) - : notFound('Nothing found.') + ? notFound(`${id} not found.`) + : notFound('Nothing found.') ); } @@ -277,8 +298,8 @@ export const destroyAll = ({ server, model, prefix = '/', config }) => { if (!list.length) { return void reply(id - ? notFound(`${id} not found.`) - : notFound('Nothing found.') + ? notFound(`${id} not found.`) + : notFound('Nothing found.') ); } diff --git a/src/get-config-for-method.js b/src/get-config-for-method.js index 0c3a28b..d6bc661 100644 --- a/src/get-config-for-method.js +++ b/src/get-config-for-method.js @@ -72,7 +72,7 @@ export const restrictMethods = [ ]; export default ({ - method, attributeValidation, modelAssociations, scopes = [], config = {}, + method, attributeValidation, associationValidation, scopes = [], config = {}, }) => { const hasWhere = whereMethods.includes(method); const hasInclude = includeMethods.includes(method); @@ -96,27 +96,9 @@ 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({ - include: [ - joi.array().items(validAssociationsString), - joi.array().items(validAssociationsObject), - validAssociationsString, - validAssociationsObject, - ], + ...associationValidation, }), get(methodConfig, 'validate.query') ); diff --git a/src/get-config-for-method.test.js b/src/get-config-for-method.test.js index 739bf8d..ea8edad 100644 --- a/src/get-config-for-method.test.js +++ b/src/get-config-for-method.test.js @@ -20,19 +20,8 @@ test.beforeEach((t) => { myKey: joi.any(), }; - const validAssociationsString = joi.string().valid(...t.context.models); - const validAssociationsObject = joi.object().keys({ - model: joi.string().valid(...t.context.models), - where: joi.object(), - }); - t.context.associationValidation = { - include: [ - joi.array().items(validAssociationsString), - joi.array().items(validAssociationsObject), - validAssociationsString, - validAssociationsObject, - ], + include: joi.array().items(joi.string().valid(t.context.models)), }; t.context.config = { @@ -162,12 +151,12 @@ test('query attributeValidation w/ config as joi object', (t) => { test('validate.query associationValidation', (t) => { const { attributeValidation, associationValidation, models } = t.context; - const modelAssociations = models; + includeMethods.forEach((method) => { const configForMethod = getConfigForMethod({ method, attributeValidation, - modelAssociations, + associationValidation, }); const { query } = configForMethod.validate; @@ -194,7 +183,6 @@ test('validate.query associationValidation', (t) => { test('query associationValidation w/ config as plain object', (t) => { const { associationValidation, models } = t.context; - const modelAssociations = models; const config = { validate: { query: { @@ -206,7 +194,7 @@ test('query associationValidation w/ config as plain object', (t) => { includeMethods.forEach((method) => { const configForMethod = getConfigForMethod({ method, - modelAssociations, + associationValidation, config, }); const { query } = configForMethod.validate; @@ -234,7 +222,6 @@ test('query associationValidation w/ config as plain object', (t) => { test('query associationValidation w/ config as joi object', (t) => { const { associationValidation, models } = t.context; - const modelAssociations = models; const queryKeys = { aKey: joi.boolean(), }; @@ -247,7 +234,7 @@ test('query associationValidation w/ config as joi object', (t) => { includeMethods.forEach((method) => { const configForMethod = getConfigForMethod({ method, - modelAssociations, + associationValidation, config, }); const { query } = configForMethod.validate; -- 2.34.1 From 49d24ea265424c87c55b25290c41ca15b9fbda61 Mon Sep 17 00:00:00 2001 From: Muhammad Labib Ramadhan Date: Tue, 8 Nov 2016 08:23:37 +0700 Subject: [PATCH 5/8] Added try catch block and JSON validation logic of relationship/association parser --- src/utils.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/utils.js b/src/utils.js index ad80096..2ea5120 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,5 +1,6 @@ import { omit, identity, toNumber, isString, isUndefined } from 'lodash'; import { notImplemented } from 'boom'; +import joi from 'joi'; const sequelizeKeys = ['include', 'order', 'limit', 'offset']; @@ -26,7 +27,14 @@ export const parseInclude = request => { if (models.isBoom) return models; return include.map(b => { - const a = /^{.*}$/.test(b) ? JSON.parse(b) : b; + let a = b; + try { + if (joi.string().regex(/^\{.*?"model":.*?\}$/)) { + a = JSON.parse(b); + } + } catch (e) { + // + } if (typeof a !== 'object') { const singluarOrPluralMatch = Object.keys(models).find((modelName) => { const { _singular, _plural } = models[modelName]; -- 2.34.1 From 6fa9e90ec5f5cfcc741f05200f586d24143ef768 Mon Sep 17 00:00:00 2001 From: Muhammad Labib Ramadhan Date: Tue, 8 Nov 2016 08:26:56 +0700 Subject: [PATCH 6/8] New line at EOF --- src/crud-include.integration.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crud-include.integration.test.js b/src/crud-include.integration.test.js index 797d89a..093dc2b 100644 --- a/src/crud-include.integration.test.js +++ b/src/crud-include.integration.test.js @@ -94,4 +94,4 @@ test('inlcude filter /teams?include[]={"model": "City", "where": {"name": "Heald const { statusCode } = await server.inject({ url, method }); t.is(statusCode, STATUS_OK); - }); \ No newline at end of file + }); -- 2.34.1 From e632f79e2b1b1425606305dc4cdd3c6a3aa4925a Mon Sep 17 00:00:00 2001 From: Muhammad Labib Ramadhan Date: Thu, 10 Nov 2016 01:46:42 +0700 Subject: [PATCH 7/8] Added support for include relationship alias (as) and nested includes, fixed wrong joi json validation implementation inside parseInclude() function, fixed some associated routes resulted 404 because of prefix option --- src/associations/one-to-many.js | 42 ++++++++-------- src/associations/one-to-one.js | 18 +++---- src/crud-include.integration.test.js | 74 +++++++++++++++++++++++++--- src/crud.js | 22 +++++---- src/utils.js | 71 ++++++++++++++++++-------- test/fixtures/models/master.js | 18 +++++++ test/fixtures/models/player.js | 4 ++ test/integration-setup.js | 33 ++++++++----- 8 files changed, 202 insertions(+), 80 deletions(-) create mode 100644 test/fixtures/models/master.js diff --git a/src/associations/one-to-many.js b/src/associations/one-to-many.js index d693172..37627f1 100644 --- a/src/associations/one-to-many.js +++ b/src/associations/one-to-many.js @@ -19,14 +19,14 @@ export default (server, a, b, names, options) => { update(server, a, b, names); }; -export const get = (server, a, b, names) => { +export const get = async (server, a, b, names) => { server.route({ method: 'GET', - path: `${prefix}/${names.a.singular}/{aid}/${names.b.singular}/{bid}`, + path: `${prefix}${names.a.singular}/{aid}/${names.b.singular}/{bid}`, @error async handler(request, reply) { - const include = parseInclude(request); + const include = await parseInclude(request); const base = await a.findOne({ where: { @@ -51,14 +51,14 @@ export const get = (server, a, b, names) => { }); }; -export const list = (server, a, b, names) => { +export const list = async (server, a, b, names) => { server.route({ method: 'GET', - path: `${prefix}/${names.a.singular}/{aid}/${names.b.plural}`, + path: `${prefix}${names.a.singular}/{aid}/${names.b.plural}`, @error async handler(request, reply) { - const include = parseInclude(request); + const include = await parseInclude(request); const where = parseWhere(request); const base = await a.findOne({ @@ -77,16 +77,16 @@ export const list = (server, a, b, names) => { }); }; -export const scope = (server, a, b, names) => { +export const scope = async (server, a, b, names) => { const scopes = Object.keys(b.options.scopes); server.route({ method: 'GET', - path: `${prefix}/${names.a.singular}/{aid}/${names.b.plural}/{scope}`, + path: `${prefix}${names.a.singular}/{aid}/${names.b.plural}/{scope}`, @error async handler(request, reply) { - const include = parseInclude(request); + const include = await parseInclude(request); const where = parseWhere(request); const base = await a.findOne({ @@ -116,7 +116,7 @@ export const scope = (server, a, b, names) => { }); }; -export const scopeScope = (server, a, b, names) => { +export const scopeScope = async (server, a, b, names) => { const scopes = { a: Object.keys(a.options.scopes), b: Object.keys(b.options.scopes), @@ -124,11 +124,11 @@ export const scopeScope = (server, a, b, names) => { server.route({ method: 'GET', - path: `${prefix}/${names.a.plural}/{scopea}/${names.b.plural}/{scopeb}`, + path: `${prefix}${names.a.plural}/{scopea}/${names.b.plural}/{scopeb}`, @error async handler(request, reply) { - const include = parseInclude(request); + const include = await parseInclude(request); const where = parseWhere(request); const list = await b.scope(request.params.scopeb).findAll({ @@ -152,14 +152,14 @@ export const scopeScope = (server, a, b, names) => { }); }; -export const destroy = (server, a, b, names) => { +export const destroy = async (server, a, b, names) => { server.route({ method: 'DELETE', - path: `${prefix}/${names.a.singular}/{aid}/${names.b.plural}`, + path: `${prefix}${names.a.singular}/{aid}/${names.b.plural}`, @error async handler(request, reply) { - const include = parseInclude(request); + const include = await parseInclude(request); const where = parseWhere(request); const base = await a.findOne({ @@ -179,16 +179,16 @@ export const destroy = (server, a, b, names) => { }); }; -export const destroyScope = (server, a, b, names) => { +export const destroyScope = async (server, a, b, names) => { const scopes = Object.keys(b.options.scopes); server.route({ method: 'DELETE', - path: `${prefix}/${names.a.singular}/{aid}/${names.b.plural}/{scope}`, + path: `${prefix}${names.a.singular}/{aid}/${names.b.plural}/{scope}`, @error async handler(request, reply) { - const include = parseInclude(request); + const include = await parseInclude(request); const where = parseWhere(request); const base = await a.findOne({ @@ -221,14 +221,14 @@ export const destroyScope = (server, a, b, names) => { }); }; -export const update = (server, a, b, names) => { +export const update = async (server, a, b, names) => { server.route({ method: 'PUT', - path: `${prefix}/${names.a.singular}/{aid}/${names.b.plural}`, + path: `${prefix}${names.a.singular}/{aid}/${names.b.plural}`, @error async handler(request, reply) { - const include = parseInclude(request); + const include = await parseInclude(request); const where = parseWhere(request); const base = await a.findOne({ diff --git a/src/associations/one-to-one.js b/src/associations/one-to-one.js index ac71e31..629de26 100644 --- a/src/associations/one-to-one.js +++ b/src/associations/one-to-one.js @@ -14,14 +14,14 @@ export default (server, a, b, names, options) => { update(server, a, b, names); }; -export const get = (server, a, b, names) => { +export const get = async (server, a, b, names) => { server.route({ method: 'GET', - path: `${prefix}/${names.a.singular}/{aid}/${names.b.singular}`, + path: `${prefix}${names.a.singular}/{aid}/${names.b.singular}`, @error async handler(request, reply) { - const include = parseInclude(request); + const include = await parseInclude(request); const where = parseWhere(request); const base = await a.findOne({ @@ -47,7 +47,7 @@ export const get = (server, a, b, names) => { export const create = (server, a, b, names) => { server.route({ method: 'POST', - path: `${prefix}/${names.a.singular}/{id}/${names.b.singular}`, + path: `${prefix}${names.a.singular}/{id}/${names.b.singular}`, @error async handler(request, reply) { @@ -67,14 +67,14 @@ export const create = (server, a, b, names) => { }); }; -export const destroy = (server, a, b, names) => { +export const destroy = async (server, a, b, names) => { server.route({ method: 'DELETE', - path: `${prefix}/${names.a.singular}/{aid}/${names.b.singular}/{bid}`, + path: `${prefix}${names.a.singular}/{aid}/${names.b.singular}/{bid}`, @error async handler(request, reply) { - const include = parseInclude(request); + const include = await parseInclude(request); const where = parseWhere(request); const base = await a.findOne({ @@ -99,11 +99,11 @@ export const destroy = (server, a, b, names) => { export const update = (server, a, b, names) => { server.route({ method: 'PUT', - path: `${prefix}/${names.a.singular}/{aid}/${names.b.singular}/{bid}`, + path: `${prefix}${names.a.singular}/{aid}/${names.b.singular}/{bid}`, @error async handler(request, reply) { - const include = parseInclude(request); + const include = await parseInclude(request); const where = parseWhere(request); const base = await a.findOne({ diff --git a/src/crud-include.integration.test.js b/src/crud-include.integration.test.js index 093dc2b..cefc774 100644 --- a/src/crud-include.integration.test.js +++ b/src/crud-include.integration.test.js @@ -6,7 +6,7 @@ const STATUS_OK = 200; setup(test); -test('belongsTo /team?include=city', async (t) => { +test('belongsTo /team?include=city', async(t) => { const { server, instances } = t.context; const { team1, city1 } = instances; const path = `/team/${team1.id}?include=city`; @@ -17,7 +17,7 @@ test('belongsTo /team?include=city', async (t) => { t.is(result.City.id, city1.id); }); -test('belongsTo /team?include=cities', async (t) => { +test('belongsTo /team?include=cities', async(t) => { const { server, instances } = t.context; const { team1, city1 } = instances; const path = `/team/${team1.id}?include=cities`; @@ -28,7 +28,7 @@ test('belongsTo /team?include=cities', async (t) => { t.is(result.City.id, city1.id); }); -test('hasMany /team?include=player', async (t) => { +test('hasMany /team?include=player', async(t) => { const { server, instances } = t.context; const { team1, player1, player2 } = instances; const path = `/team/${team1.id}?include=player`; @@ -42,7 +42,7 @@ test('hasMany /team?include=player', async (t) => { t.truthy(playerIds.includes(player2.id)); }); -test('hasMany /team?include=players', async (t) => { +test('hasMany /team?include=players', async(t) => { const { server, instances } = t.context; const { team1, player1, player2 } = instances; const path = `/team/${team1.id}?include=players`; @@ -56,7 +56,18 @@ test('hasMany /team?include=players', async (t) => { t.truthy(playerIds.includes(player2.id)); }); -test('multiple includes /team?include=players&include=city', async (t) => { +test('belongsTo with alias /player?include={"model": "Master", "as": "Coach"}', async(t) => { + const { server, instances } = t.context; + const { team1, master1 } = instances; + const path = `/player/${team1.id}?include={"model": "Master", "as": "Coach"}`; + + const { result, statusCode } = await server.inject(path); + t.is(statusCode, STATUS_OK); + t.is(result.id, team1.id); + t.is(result.Coach.id, master1.id); +}); + +test('multiple includes /team?include=players&include=city', async(t) => { const { server, instances } = t.context; const { team1, player1, player2, city1 } = instances; const path = `/team/${team1.id}?include=players&include=city`; @@ -71,7 +82,7 @@ test('multiple includes /team?include=players&include=city', async (t) => { t.is(result.City.id, city1.id); }); -test('multiple includes /team?include[]=players&include[]=city', async (t) => { +test('multiple includes /team?include[]=players&include[]=city', async(t) => { const { server, instances } = t.context; const { team1, player1, player2, city1 } = instances; const path = `/team/${team1.id}?include[]=players&include[]=city`; @@ -86,8 +97,23 @@ test('multiple includes /team?include[]=players&include[]=city', async (t) => { t.is(result.City.id, city1.id); }); +test('multiple includes /team?include[]=players&include[]={"model": "City"}', async(t) => { + const { server, instances } = t.context; + const { team1, player1, player2, city1 } = instances; + const path = `/team/${team1.id}?include[]=players&include[]={"model": "City"}`; + + const { result, statusCode } = await server.inject(path); + t.is(statusCode, STATUS_OK); + t.is(result.id, team1.id); + + const playerIds = result.Players.map(({ id }) => id); + t.truthy(playerIds.includes(player1.id)); + 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) => { + , async(t) => { const { server } = t.context; const url = '/teams?include[]={"model": "City", "where": {"name": "Healdsburg"}}'; const method = 'GET'; @@ -95,3 +121,37 @@ test('inlcude filter /teams?include[]={"model": "City", "where": {"name": "Heald const { statusCode } = await server.inject({ url, method }); 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"}}}' + , 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 method = 'GET'; + + const { result, statusCode } = await server.inject({ url, method }); + t.is(statusCode, STATUS_OK); + t.is(result.id, city1.id); + + const teamIds = result.Teams.map(({ id }) => id); + t.truthy(teamIds.includes(team1.id)); + t.truthy(teamIds.includes(team2.id)); +}); diff --git a/src/crud.js b/src/crud.js index 3037a2e..f7e0c72 100644 --- a/src/crud.js +++ b/src/crud.js @@ -56,6 +56,7 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi const modelName = model._singular; const modelAttributes = Object.keys(model.attributes); const associatedModelNames = Object.keys(model.associations); + const associatedModelAliases = _.map(model.associations, (assoc => assoc.as)); const modelAssociations = [ ...associatedModelNames, ..._.flatMap(associatedModelNames, (associationName) => { @@ -82,12 +83,13 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi ...attributeValidation, ...sequelizeOperators, }), + as: joi.string().valid(...associatedModelAliases), + include: joi.any(), // @Todo: should validate the same as associationValidation var below }) : joi.valid(null); const associationValidation = { include: [ - joi.array().items(validAssociationsString), - joi.array().items(validAssociationsObject), + joi.array().items(validAssociationsString, validAssociationsObject), validAssociationsString, validAssociationsObject, ], @@ -161,14 +163,14 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi } }; -export const list = ({ server, model, prefix = '/', config }) => { +export const list = async ({ server, model, prefix = '/', config }) => { server.route({ method: 'GET', path: path.join(prefix, model._plural), @error async handler(request, reply) { - const include = parseInclude(request); + const include = await parseInclude(request); const where = parseWhere(request); const { limit, offset } = parseLimitAndOffset(request); const order = parseOrder(request); @@ -188,14 +190,14 @@ export const list = ({ server, model, prefix = '/', config }) => { }); }; -export const get = ({ server, model, prefix = '/', config }) => { +export const get = async ({ server, model, prefix = '/', config }) => { server.route({ method: 'GET', path: path.join(prefix, model._singular, '{id?}'), @error async handler(request, reply) { - const include = parseInclude(request); + const include = await parseInclude(request); const where = parseWhere(request); const { id } = request.params; if (id) where[model.primaryKeyField] = id; @@ -212,14 +214,14 @@ export const get = ({ server, model, prefix = '/', config }) => { }); }; -export const scope = ({ server, model, prefix = '/', config }) => { +export const scope = async ({ server, model, prefix = '/', config }) => { server.route({ method: 'GET', path: path.join(prefix, model._plural, '{scope}'), @error async handler(request, reply) { - const include = parseInclude(request); + const include = await parseInclude(request); const where = parseWhere(request); const { limit, offset } = parseLimitAndOffset(request); const order = parseOrder(request); @@ -313,14 +315,14 @@ export const destroyAll = ({ server, model, prefix = '/', config }) => { }); }; -export const destroyScope = ({ server, model, prefix = '/', config }) => { +export const destroyScope = async ({ server, model, prefix = '/', config }) => { server.route({ method: 'DELETE', path: path.join(prefix, model._plural, '{scope}'), @error async handler(request, reply) { - const include = parseInclude(request); + const include = await parseInclude(request); const where = parseWhere(request); if (include instanceof Error) return void reply(include); diff --git a/src/utils.js b/src/utils.js index 2ea5120..4ea6481 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,6 +1,7 @@ import { omit, identity, toNumber, isString, isUndefined } from 'lodash'; import { notImplemented } from 'boom'; import joi from 'joi'; +import Promise from 'bluebird'; const sequelizeKeys = ['include', 'order', 'limit', 'offset']; @@ -9,7 +10,7 @@ const getModels = (request) => { const noRequestModels = !request.models; if (noGetDb && noRequestModels) { return notImplemented('`request.getDb` or `request.models` are not defined.' - + 'Be sure to load hapi-sequelize before hapi-sequelize-crud.'); + + 'Be sure to load hapi-sequelize before hapi-sequelize-crud.'); } const { models } = noGetDb ? request : request.getDb(); @@ -17,41 +18,69 @@ const getModels = (request) => { return models; }; -export const parseInclude = request => { +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] + ? request.query.include + : [request.query.include] ; const models = getModels(request); if (models.isBoom) return models; - return include.map(b => { + 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; try { - if (joi.string().regex(/^\{.*?"model":.*?\}$/)) { + if (!jsonValidation.validate(a).error) { a = JSON.parse(b); } } catch (e) { // } - 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 (typeof a === 'string') return models[a]; - - if (a && typeof a.model === 'string' && a.model.length) { - a.model = models[a.model]; - } - - return a; + return getModelInstance(a); }).filter(identity); + + return await Promise.all(includes); }; export const parseWhere = request => { diff --git a/test/fixtures/models/master.js b/test/fixtures/models/master.js new file mode 100644 index 0000000..9e221fd --- /dev/null +++ b/test/fixtures/models/master.js @@ -0,0 +1,18 @@ +export default (sequelize, DataTypes) => { + return sequelize.define('Master', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + name: DataTypes.STRING, + }, { + classMethods: { + associate: (models) => { + models.Master.hasMany(models.Player, { + foreignKey: 'coachId' + }); + }, + }, + }); +}; diff --git a/test/fixtures/models/player.js b/test/fixtures/models/player.js index 0bc1d64..cf325a5 100644 --- a/test/fixtures/models/player.js +++ b/test/fixtures/models/player.js @@ -14,6 +14,10 @@ export default (sequelize, DataTypes) => { models.Player.belongsTo(models.Team, { foreignKey: { name: 'teamId' }, }); + models.Player.belongsTo(models.Master, { + foreignKey: 'coachId', + as: 'Coach', + }); }, }, scopes: { diff --git a/test/integration-setup.js b/test/integration-setup.js index 190f958..64a5c84 100644 --- a/test/integration-setup.js +++ b/test/integration-setup.js @@ -14,15 +14,16 @@ const modelNames = [ { Singluar: 'City', singular: 'city', Plural: 'Cities', plural: 'cities' }, { Singluar: 'Team', singular: 'team', Plural: 'Teams', plural: 'teams' }, { Singluar: 'Player', singular: 'player', Plural: 'Players', plural: 'players' }, + { Singluar: 'Master', singular: 'master', Plural: 'Masters', plural: 'masters' }, ]; export default (test) => { - test.beforeEach('get an open port', async (t) => { + test.beforeEach('get an open port', async(t) => { t.context.port = await getPort(); }); - test.beforeEach('setup server', async (t) => { + test.beforeEach('setup server', async(t) => { const sequelize = t.context.sequelize = new Sequelize({ dialect: 'sqlite', logging: false, @@ -46,25 +47,33 @@ export default (test) => { }); await server.register({ - register: require('../src/index.js'), - options: { - name: dbName, + register: require('../src/index.js'), + options: { + name: dbName, + }, }, - }, ); }); - test.beforeEach('create data', async (t) => { - const { Player, Team, City } = t.context.sequelize.models; + test.beforeEach('create data', async(t) => { + const { Player, Master, Team, City } = t.context.sequelize.models; const city1 = await City.create({ name: 'Healdsburg' }); const team1 = await Team.create({ name: 'Baseballs', cityId: city1.id }); const team2 = await Team.create({ name: 'Footballs', cityId: city1.id }); + const master1 = await Master.create({ name: 'Shifu' }); + const master2 = await Master.create({ name: 'Oogway' }); const player1 = await Player.create({ - name: 'Cat', teamId: team1.id, active: true, + name: 'Cat', teamId: team1.id, active: true, coachId: master1.id }); - const player2 = await Player.create({ name: 'Pinot', teamId: team1.id }); - const player3 = await Player.create({ name: 'Syrah', teamId: team2.id }); - t.context.instances = { city1, team1, team2, player1, player2, player3 }; + const player2 = await Player.create({ + name: 'Pinot', teamId: team1.id, coachId: master1.id + }); + const player3 = await Player.create({ + name: 'Syrah', teamId: team2.id, coachId: master2.id + }); + t.context.instances = { + city1, team1, team2, player1, player2, player3, master1, master2 + }; }); // kill the server so that we can exit and don't leak memory -- 2.34.1 From 05793eb7499a1393058a2e35b2c46aefa3994345 Mon Sep 17 00:00:00 2001 From: Muhammad Labib Ramadhan Date: Fri, 11 Nov 2016 09:03:01 +0700 Subject: [PATCH 8/8] 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); -- 2.34.1