From 11291f0e082f63d5a8e2e45247d0cb299d474da6 Mon Sep 17 00:00:00 2001 From: Mahdi Dibaiee Date: Thu, 10 Mar 2016 10:48:30 +0330 Subject: [PATCH] refactor: minimize repeated code by re-using `parseInclude`, `parseWhere` and `getMethod` feat(include): ability to specify multiple includes, as an array --- README.md | 4 + package.json | 2 +- src/associations/associate.js | 14 ++- src/associations/one-to-many.js | 203 ++++++++++++-------------------- src/associations/one-to-one.js | 104 ++++++++-------- src/crud.js | 78 +++--------- src/index.js | 39 ++++-- src/utils.js | 39 ++++++ 8 files changed, 226 insertions(+), 257 deletions(-) create mode 100644 src/utils.js diff --git a/README.md b/README.md index d48a7d3..66872eb 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,10 @@ DELETE /role/{id}/teams?members=5 DELETE /team/{id}/role/{id} DELETE /role/{id}/team/{id} +# include +# include nested associations (you can specify an array if includes) +GET /team/{id}/role/{id}?include=SomeRoleAssociation + # you also get routes to associate objects with each other GET /associate/role/{id}/employee/{id} # associates role {id} with employee {id} diff --git a/package.json b/package.json index a84e65f..48962a7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hapi-sequelize-crud", - "version": "1.5.2", + "version": "2.0.0", "description": "Hapi plugin that automatically generates RESTful API for CRUD", "main": "build/index.js", "config": { diff --git a/src/associations/associate.js b/src/associations/associate.js index ad48069..a8dda0c 100644 --- a/src/associations/associate.js +++ b/src/associations/associate.js @@ -1,15 +1,16 @@ import joi from 'joi'; import error from '../error'; import { capitalize } from 'lodash/string'; +import { getMethod } from '../utils'; let prefix; -export default (server, a, b, options) => { +export default (server, a, b, names, options) => { prefix = options.prefix; server.route({ method: 'GET', - path: `${prefix}/associate/${a._singular}/{aid}/${b._singular}/{bid}`, + path: `${prefix}/associate/${names.a.singular}/{aid}/${names.b.singular}/{bid}`, @error async handler(request, reply) { @@ -25,12 +26,15 @@ export default (server, a, b, options) => { } }); - let fna = (instancea['add' + b.name] || instancea['set' + b.name]).bind(instancea); - let fnb = (instanceb['add' + a.name] || instanceb['set' + a.name]).bind(instanceb); + const fna = getMethod(instancea, names.b, false, 'add') || + getMethod(instancea, names.b, false, 'set'); + const fnb = getMethod(instanceb, names.a, false, 'add') || + getMethod(instanceb, names.a, false, 'set'); + await fna(instanceb); await fnb(instancea); - reply(instancea); + reply([instancea, instanceb]); } }) } diff --git a/src/associations/one-to-many.js b/src/associations/one-to-many.js index 0c4a8c0..22eda4a 100644 --- a/src/associations/one-to-many.js +++ b/src/associations/one-to-many.js @@ -1,118 +1,94 @@ import joi from 'joi'; import error from '../error'; import _ from 'lodash'; +import { parseInclude, parseWhere, getMethod } from '../utils'; let prefix; -export default (server, a, b, options) => { +export default (server, a, b, names, options) => { prefix = options.prefix; - get(server, a, b); - list(server, a, b); - scope(server, a, b); - scopeScope(server, a, b); - destroy(server, a, b); - destroyScope(server, a, b); - update(server, a, b); + get(server, a, b, names); + list(server, a, b, names); + scope(server, a, b, names); + scopeScope(server, a, b, names); + destroy(server, a, b, names); + destroyScope(server, a, b, names); + update(server, a, b, names); } -export const get = (server, a, b) => { +export const get = (server, a, b, names) => { server.route({ method: 'GET', - path: `${prefix}/${a._singular}/{aid}/${b._singular}/{bid}`, + path: `${prefix}/${names.a.singular}/{aid}/${names.b.singular}/{bid}`, @error async handler(request, reply) { - let include = []; - if (request.query.include) - include = [request.models[request.query.include]]; + const include = parseInclude(request); - let instance = await b.findOne({ + const base = a.findOne({ where: { - id: request.params.bid - }, - - include: include.concat({ - where: { - id: request.params.aid - }, - model: a - }) + id: request.params.aid + } }); + const method = getMethod(base, names.b); + const list = await method({ where: { + id: request.params.bid + }, include }); + reply(list); } }) } -export const list = (server, a, b) => { +export const list = (server, a, b, names) => { server.route({ method: 'GET', - path: `${prefix}/${a._singular}/{aid}/${b._plural}`, + path: `${prefix}/${names.a.singular}/{aid}/${names.b.plural}`, @error async handler(request, reply) { - let include = []; - if (request.query.include) - include = [request.models[request.query.include]]; + const include = parseInclude(request); + const where = parseWhere(request); - let where = _.omit(request.query, 'include'); - - for (const key of Object.keys(where)) { - try { - where[key] = JSON.parse(where[key]); - } catch (e) { - // + const base = await a.findOne({ + where: { + id: request.params.aid } - } - - let list = await b.findAll({ - where, - - include: include.concat({ - where: { - id: request.params.aid - }, - model: a - }) }); + const method = getMethod(base, names.b); + const list = await method({ where, include }); + reply(list); } }) } -export const scope = (server, a, b) => { +export const scope = (server, a, b, names) => { let scopes = Object.keys(b.options.scopes); server.route({ method: 'GET', - path: `${prefix}/${a._singular}/{aid}/${b._plural}/{scope}`, + path: `${prefix}/${names.a.singular}/{aid}/${names.b.plural}/{scope}`, @error async handler(request, reply) { - let include = []; - if (request.query.include) - include = [request.models[request.query.include]]; + const include = parseInclude(request); + const where = parseWhere(request); - let where = _.omit(request.query, 'include'); - - for (const key of Object.keys(where)) { - try { - where[key] = JSON.parse(where[key]); - } catch (e) { - // + const base = await a.findOne({ + where: { + id: request.params.aid } - } + }); - let list = await b.scope(request.params.scope).findAll({ + const method = getMethod(base, names.b); + const list = await method({ + scope: request.params.scope, where, - include: include.concat({ - where: { - id: request.params.aid - }, - model: a - }) + include }); reply(list); @@ -129,7 +105,7 @@ export const scope = (server, a, b) => { }) } -export const scopeScope = (server, a, b) => { +export const scopeScope = (server, a, b, names) => { let scopes = { a: Object.keys(a.options.scopes), b: Object.keys(b.options.scopes) @@ -137,23 +113,12 @@ export const scopeScope = (server, a, b) => { server.route({ method: 'GET', - path: `${prefix}/${a._plural}/{scopea}/${b._plural}/{scopeb}`, + path: `${prefix}/${names.a.plural}/{scopea}/${names.b.plural}/{scopeb}`, @error async handler(request, reply) { - let include = []; - if (request.query.include) - include = [request.models[request.query.include]]; - - let where = _.omit(request.query, 'include'); - - for (const key of Object.keys(where)) { - try { - where[key] = JSON.parse(where[key]); - } catch (e) { - // - } - } + const include = parseInclude(request); + const where = parseWhere(request); let list = await b.scope(request.params.scopeb).findAll({ where, @@ -176,33 +141,23 @@ export const scopeScope = (server, a, b) => { }) } -export const destroy = (server, a, b) => { +export const destroy = (server, a, b, names) => { server.route({ method: 'DELETE', - path: `${prefix}/${a._singular}/{aid}/${b._plural}`, + path: `${prefix}/${names.a.singular}/{aid}/${names.b.plural}`, @error async handler(request, reply) { - let where = _.omit(request.query, 'include'); + const include = parseInclude(request); + const where = parseWhere(request); - for (const key of Object.keys(where)) { - try { - where[key] = JSON.parse(where[key]); - } catch (e) { - // - } - } - - let list = await b.findAll({ - where, - include: { - model: a, - where: { - id: request.params.aid - } - } + const base = await a.findOne({ + where: request.params.aid }); + const method = getMethod(base, names.b); + const list = await method({ where, include }); + await* list.map(instance => instance.destroy()); reply(list); @@ -210,34 +165,29 @@ export const destroy = (server, a, b) => { }) } -export const destroyScope = (server, a, b) => { +export const destroyScope = (server, a, b, names) => { let scopes = Object.keys(b.options.scopes); server.route({ method: 'DELETE', - path: `${prefix}/${a._singular}/{aid}/${b._plural}/{scope}`, + path: `${prefix}/${names.a.singular}/{aid}/${names.b.plural}/{scope}`, @error async handler(request, reply) { - let where = _.omit(request.query, 'include'); + const include = parseInclude(request); + const where = parseWhere(request); - for (const key of Object.keys(where)) { - try { - where[key] = JSON.parse(where[key]); - } catch (e) { - // + const base = await a.findOne({ + where: { + id: request.params.aid } - } + }); - let list = await b.scope(request.params.scope).findAll({ + const method = getMethod(base, names.b); + const list = await method({ + scope: request.params.scope, where, - - include: { - model: a, - where: { - id: request.params.aid - } - } + include }); await* list.map(instance => instance.destroy()); @@ -256,24 +206,25 @@ export const destroyScope = (server, a, b) => { }); } -export const update = (server, a, b) => { +export const update = (server, a, b, names) => { server.route({ method: 'PUT', - path: `${prefix}/${a._singular}/{aid}/${b._plural}`, + path: `${prefix}/${names.a.singular}/{aid}/${names.b.plural}`, @error async handler(request, reply) { - let list = await b.findOne({ - include: { - model: a, - where: { - id: request.params.aid, + const include = parseInclude(request); + const where = parseWhere(request); - ...request.query - } + const base = await a.findOne({ + where: { + id: request.params.aid } }); + const method = getMethod(base, names.b); + const list = await method({ where, include }); + await* list.map(instance => instance.update(request.payload)); reply(list); diff --git a/src/associations/one-to-one.js b/src/associations/one-to-one.js index 1ccd8fc..6994667 100644 --- a/src/associations/one-to-one.js +++ b/src/associations/one-to-one.js @@ -1,82 +1,87 @@ import joi from 'joi'; import error from '../error'; import _ from 'lodash'; +import { parseInclude, parseWhere, getMethod } from '../utils'; let prefix; -export default (server, a, b, options) => { +export default (server, a, b, names, options) => { prefix = options.prefix; - get(server, a, b); - create(server, a, b); - destroy(server, a, b); - update(server, a, b); + get(server, a, b, names); + create(server, a, b, names); + destroy(server, a, b, names); + update(server, a, b, names); } -export const get = (server, a, b) => { +export const get = (server, a, b, names) => { server.route({ method: 'GET', - path: `${prefix}/${a._singular}/{aid}/${b._singular}`, + path: `${prefix}/${names.a.singular}/{aid}/${names.b.singular}`, @error async handler(request, reply) { - let include = []; - if (request.query.include) - include = [request.models[request.query.include]]; + const include = parseInclude(request); + const where = parseWhere(request); - let where = _.omit(request.query, 'include'); - - let [instance] = await b.findAll({ - where, - - include: include.concat({ - model: a, - where: { - id: request.params.aid - } - }) + const base = await a.findOne({ + where: { + id: request.params.aid + } }); + const method = getMethod(base, names.b, false); - reply(instance); + const list = await method({ where, include, limit: 1 }); + + if (Array.isArray(list)) { + reply(list[0]); + } else { + reply(list); + } } }) } -export const create = (server, a, b) => { +export const create = (server, a, b, names) => { server.route({ method: 'POST', - path: `${prefix}/${a._singular}/{id}/${b._singular}`, + path: `${prefix}/${names.a.singular}/{id}/${names.b.singular}`, @error async handler(request, reply) { - request.payload[a.name + 'Id'] = request.params.id; - let instance = await request.models[b.name].create(request.payload); + const base = await a.findOne({ + where: { + id: request.params.id + } + }); + + const method = getMethod(base, names.b, false, 'create'); + const instance = await method(request.payload); reply(instance); } }) } -export const destroy = (server, a, b) => { +export const destroy = (server, a, b, names) => { server.route({ method: 'DELETE', - path: `${prefix}/${a._singular}/{aid}/${b._singular}/{bid}`, + path: `${prefix}/${names.a.singular}/{aid}/${names.b.singular}/{bid}`, @error async handler(request, reply) { - let instance = await b.findOne({ - where: { - id: request.params.bid - }, + const include = parseInclude(request); + const where = parseWhere(request); - include: [{ - model: a, - where: { - id: request.params.aid - } - }] + const base = await a.findOne({ + where: { + id: request.params.aid + } }); + const method = getMethod(base, names.b, false); + + const instance = await method({ where, include }); await instance.destroy(); reply(instance); @@ -84,26 +89,25 @@ export const destroy = (server, a, b) => { }) } -export const update = (server, a, b) => { +export const update = (server, a, b, names) => { server.route({ method: 'PUT', - path: `${prefix}/${a._singular}/{aid}/${b._singular}/{bid}`, + path: `${prefix}/${names.a.singular}/{aid}/${names.b.singular}/{bid}`, @error async handler(request, reply) { - let instance = await b.findOne({ - where: { - id: request.params.bid - }, + const include = parseInclude(request); + const where = parseWhere(request); - include: [{ - model: a, - where: { - id: request.params.aid - } - }] + const base = await a.findOne({ + where: { + id: request.params.aid + } }); + const method = getMethod(base, names.b, false); + + const instance = await method({ where, include }); await instance.update(request.payload); reply(instance); diff --git a/src/crud.js b/src/crud.js index db5429f..d95a92c 100644 --- a/src/crud.js +++ b/src/crud.js @@ -1,6 +1,7 @@ import joi from 'joi'; import error from './error'; import _ from 'lodash'; +import { parseInclude, parseWhere } from './utils'; let prefix; @@ -23,20 +24,10 @@ export const list = (server, model) => { @error async handler(request, reply) { - if (request.query.include) - var include = [request.models[request.query.include]]; + const include = parseInclude(request); + const where = parseWhere(request); - let where = _.omit(request.query, 'include'); - - for (const key of Object.keys(where)) { - try { - where[key] = JSON.parse(where[key]); - } catch (e) { - // - } - } - - let list = await model.findAll({ + const list = await model.findAll({ where, include }); @@ -52,20 +43,10 @@ export const get = (server, model) => { @error async handler(request, reply) { - if (request.query.include) - var include = [request.models[request.query.include]]; + const include = parseInclude(request); + const where = parseWhere(request); - let where = request.params.id ? { id : request.params.id } : _.omit(request.query, 'include'); - - for (const key of Object.keys(where)) { - try { - where[key] = JSON.parse(where[key]); - } catch (e) { - // - } - } - - let instance = await model.findOne({ where, include }); + const instance = await model.findOne({ where, include }); reply(instance); }, @@ -88,20 +69,10 @@ export const scope = (server, model) => { @error async handler(request, reply) { - if (request.query.include) - var include = [request.models[request.query.include]]; + const include = parseInclude(request); + const where = parseWhere(request); - let where = _.omit(request.query, 'include'); - - for (const key of Object.keys(where)) { - try { - where[key] = JSON.parse(where[key]); - } catch (e) { - // - } - } - - let list = await model.scope(request.params.scope).findAll({ include, where }); + const list = await model.scope(request.params.scope).findAll({ include, where }); reply(list); }, @@ -122,7 +93,7 @@ export const create = (server, model) => { @error async handler(request, reply) { - let instance = await model.create(request.payload); + const instance = await model.create(request.payload); reply(instance); } @@ -136,17 +107,10 @@ export const destroy = (server, model) => { @error async handler(request, reply) { - let where = request.params.id ? { id : request.params.id } : request.query; + const include = parseInclude(request); + const where = parseWhere(request); - for (const key of Object.keys(where)) { - try { - where[key] = JSON.parse(where[key]); - } catch (e) { - // - } - } - - let list = await model.findAll({ where }); + const list = await model.findAll({ where }); await* list.map(instance => instance.destroy()); @@ -164,18 +128,8 @@ export const destroyScope = (server, model) => { @error async handler(request, reply) { - if (request.query.include) - var include = [request.models[request.query.include]]; - - let where = _.omit(request.query, 'include'); - - for (const key of Object.keys(where)) { - try { - where[key] = JSON.parse(where[key]); - } catch (e) { - // - } - } + const include = parseInclude(request); + const where = parseWhere(request); let list = await model.scope(request.params.scope).findAll({ include, where }); diff --git a/src/index.js b/src/index.js index 288e501..1c64c4a 100644 --- a/src/index.js +++ b/src/index.js @@ -36,12 +36,25 @@ const register = (server, options = {}, next) => { for (let key of Object.keys(model.associations)) { let association = model.associations[key]; let { associationType, source, target } = association; + // console.dir(association, null, { depth: null }); let sourceName = source.options.name; let targetName = target.options.name; - target._plural = targetName.plural.toLowerCase(); - target._singular = targetName.singular.toLowerCase(); + + const names = (rev) => { + const arr = [{ + plural: sourceName.plural.toLowerCase(), + singular: sourceName.singular.toLowerCase(), + original: sourceName + }, { + plural: association.options.name.plural.toLowerCase(), + singular: association.options.name.singular.toLowerCase(), + original: association.options.name + }]; + + return rev ? { b: arr[0], a: arr[1] } : { a: arr[0], b: arr[1] }; + } let targetAssociations = target.associations[sourceName.plural] || target.associations[sourceName.singular]; let sourceType = association.associationType, @@ -49,26 +62,26 @@ const register = (server, options = {}, next) => { try { if (sourceType === 'BelongsTo' && (targetType === 'BelongsTo' || !targetType)) { - associations.oneToOne(server, source, target, options); - associations.oneToOne(server, target, source, options); + associations.oneToOne(server, source, target, names(), options); + associations.oneToOne(server, target, source, names(1), options); } if (sourceType === 'BelongsTo' && targetType === 'HasMany') { - associations.oneToOne(server, source, target, options); - associations.oneToOne(server, target, source, options); - associations.oneToMany(server, target, source, options); + associations.oneToOne(server, source, target, names(), options); + associations.oneToOne(server, target, source, names(1), options); + associations.oneToMany(server, target, source, names(1), options); } if (sourceType === 'BelongsToMany' && targetType === 'BelongsToMany') { - associations.oneToOne(server, source, target, options); - associations.oneToOne(server, target, source, options); + associations.oneToOne(server, source, target, names(), options); + associations.oneToOne(server, target, source, names(1), options); - associations.oneToMany(server, source, target, options); - associations.oneToMany(server, target, source, options); + associations.oneToMany(server, source, target, names(), options); + associations.oneToMany(server, target, source, names(1), options); } - associations.associate(server, source, target, options); - associations.associate(server, target, source, options); + associations.associate(server, source, target, names(), options); + associations.associate(server, target, source, names(1), options); } catch(e) { // There might be conflicts in case of models associated with themselves and some other // rare cases. diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..065a3cc --- /dev/null +++ b/src/utils.js @@ -0,0 +1,39 @@ +import { omit } from 'lodash'; + +export const parseInclude = request => { + const include = Array.isArray(request.query.include) ? request.query.include + : [request.query.include]; + + return include.map(a => { + if (typeof a === 'string') return request.models[a]; + + if (a && typeof a.model === 'string' && a.model.length) { + a.model = request.models[a.model]; + } + + return a; + }).filter(a => a); +} + +export const parseWhere = request => { + const where = omit(request.query, 'include'); + + for (const key of Object.keys(where)) { + try { + where[key] = JSON.parse(where[key]); + } catch (e) { + // + } + } + + return where; +} + +export const getMethod = (model, association, plural = true, method = 'get') => { + const a = plural ? association.original.plural : association.original.singular; + const b = plural ? association.original.singular : association.original.plural; // alternative + const fn = model[`${method}${a}`] || model[`${method}${b}`]; + if (fn) return fn.bind(model); + + return false; +}