refactor: minimize repeated code by re-using parseInclude, parseWhere and getMethod

feat(include): ability to specify multiple includes, as an array
This commit is contained in:
Mahdi Dibaiee 2016-03-10 10:48:30 +03:30
parent 00e8e89767
commit 11291f0e08
8 changed files with 226 additions and 257 deletions

View File

@ -78,6 +78,10 @@ DELETE /role/{id}/teams?members=5
DELETE /team/{id}/role/{id} DELETE /team/{id}/role/{id}
DELETE /role/{id}/team/{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 # you also get routes to associate objects with each other
GET /associate/role/{id}/employee/{id} # associates role {id} with employee {id} GET /associate/role/{id}/employee/{id} # associates role {id} with employee {id}

View File

@ -1,6 +1,6 @@
{ {
"name": "hapi-sequelize-crud", "name": "hapi-sequelize-crud",
"version": "1.5.2", "version": "2.0.0",
"description": "Hapi plugin that automatically generates RESTful API for CRUD", "description": "Hapi plugin that automatically generates RESTful API for CRUD",
"main": "build/index.js", "main": "build/index.js",
"config": { "config": {

View File

@ -1,15 +1,16 @@
import joi from 'joi'; import joi from 'joi';
import error from '../error'; import error from '../error';
import { capitalize } from 'lodash/string'; import { capitalize } from 'lodash/string';
import { getMethod } from '../utils';
let prefix; let prefix;
export default (server, a, b, options) => { export default (server, a, b, names, options) => {
prefix = options.prefix; prefix = options.prefix;
server.route({ server.route({
method: 'GET', method: 'GET',
path: `${prefix}/associate/${a._singular}/{aid}/${b._singular}/{bid}`, path: `${prefix}/associate/${names.a.singular}/{aid}/${names.b.singular}/{bid}`,
@error @error
async handler(request, reply) { 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); const fna = getMethod(instancea, names.b, false, 'add') ||
let fnb = (instanceb['add' + a.name] || instanceb['set' + a.name]).bind(instanceb); getMethod(instancea, names.b, false, 'set');
const fnb = getMethod(instanceb, names.a, false, 'add') ||
getMethod(instanceb, names.a, false, 'set');
await fna(instanceb); await fna(instanceb);
await fnb(instancea); await fnb(instancea);
reply(instancea); reply([instancea, instanceb]);
} }
}) })
} }

View File

@ -1,118 +1,94 @@
import joi from 'joi'; import joi from 'joi';
import error from '../error'; import error from '../error';
import _ from 'lodash'; import _ from 'lodash';
import { parseInclude, parseWhere, getMethod } from '../utils';
let prefix; let prefix;
export default (server, a, b, options) => { export default (server, a, b, names, options) => {
prefix = options.prefix; prefix = options.prefix;
get(server, a, b); get(server, a, b, names);
list(server, a, b); list(server, a, b, names);
scope(server, a, b); scope(server, a, b, names);
scopeScope(server, a, b); scopeScope(server, a, b, names);
destroy(server, a, b); destroy(server, a, b, names);
destroyScope(server, a, b); destroyScope(server, a, b, names);
update(server, a, b); update(server, a, b, names);
} }
export const get = (server, a, b) => { export const get = (server, a, b, names) => {
server.route({ server.route({
method: 'GET', method: 'GET',
path: `${prefix}/${a._singular}/{aid}/${b._singular}/{bid}`, path: `${prefix}/${names.a.singular}/{aid}/${names.b.singular}/{bid}`,
@error @error
async handler(request, reply) { async handler(request, reply) {
let include = []; const include = parseInclude(request);
if (request.query.include)
include = [request.models[request.query.include]];
let instance = await b.findOne({ const base = a.findOne({
where: { where: {
id: request.params.bid id: request.params.aid
}, }
include: include.concat({
where: {
id: request.params.aid
},
model: a
})
}); });
const method = getMethod(base, names.b);
const list = await method({ where: {
id: request.params.bid
}, include });
reply(list); reply(list);
} }
}) })
} }
export const list = (server, a, b) => { export const list = (server, a, b, names) => {
server.route({ server.route({
method: 'GET', method: 'GET',
path: `${prefix}/${a._singular}/{aid}/${b._plural}`, path: `${prefix}/${names.a.singular}/{aid}/${names.b.plural}`,
@error @error
async handler(request, reply) { async handler(request, reply) {
let include = []; const include = parseInclude(request);
if (request.query.include) const where = parseWhere(request);
include = [request.models[request.query.include]];
let where = _.omit(request.query, 'include'); const base = await a.findOne({
where: {
for (const key of Object.keys(where)) { id: request.params.aid
try {
where[key] = JSON.parse(where[key]);
} catch (e) {
//
} }
}
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); reply(list);
} }
}) })
} }
export const scope = (server, a, b) => { export const scope = (server, a, b, names) => {
let scopes = Object.keys(b.options.scopes); let scopes = Object.keys(b.options.scopes);
server.route({ server.route({
method: 'GET', method: 'GET',
path: `${prefix}/${a._singular}/{aid}/${b._plural}/{scope}`, path: `${prefix}/${names.a.singular}/{aid}/${names.b.plural}/{scope}`,
@error @error
async handler(request, reply) { async handler(request, reply) {
let include = []; const include = parseInclude(request);
if (request.query.include) const where = parseWhere(request);
include = [request.models[request.query.include]];
let where = _.omit(request.query, 'include'); const base = await a.findOne({
where: {
for (const key of Object.keys(where)) { id: request.params.aid
try {
where[key] = JSON.parse(where[key]);
} catch (e) {
//
} }
} });
let list = await b.scope(request.params.scope).findAll({ const method = getMethod(base, names.b);
const list = await method({
scope: request.params.scope,
where, where,
include: include.concat({ include
where: {
id: request.params.aid
},
model: a
})
}); });
reply(list); 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 = { let scopes = {
a: Object.keys(a.options.scopes), a: Object.keys(a.options.scopes),
b: Object.keys(b.options.scopes) b: Object.keys(b.options.scopes)
@ -137,23 +113,12 @@ export const scopeScope = (server, a, b) => {
server.route({ server.route({
method: 'GET', method: 'GET',
path: `${prefix}/${a._plural}/{scopea}/${b._plural}/{scopeb}`, path: `${prefix}/${names.a.plural}/{scopea}/${names.b.plural}/{scopeb}`,
@error @error
async handler(request, reply) { async handler(request, reply) {
let include = []; const include = parseInclude(request);
if (request.query.include) const where = parseWhere(request);
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) {
//
}
}
let list = await b.scope(request.params.scopeb).findAll({ let list = await b.scope(request.params.scopeb).findAll({
where, 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({ server.route({
method: 'DELETE', method: 'DELETE',
path: `${prefix}/${a._singular}/{aid}/${b._plural}`, path: `${prefix}/${names.a.singular}/{aid}/${names.b.plural}`,
@error @error
async handler(request, reply) { 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)) { const base = await a.findOne({
try { where: request.params.aid
where[key] = JSON.parse(where[key]);
} catch (e) {
//
}
}
let list = await b.findAll({
where,
include: {
model: a,
where: {
id: request.params.aid
}
}
}); });
const method = getMethod(base, names.b);
const list = await method({ where, include });
await* list.map(instance => instance.destroy()); await* list.map(instance => instance.destroy());
reply(list); 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); let scopes = Object.keys(b.options.scopes);
server.route({ server.route({
method: 'DELETE', method: 'DELETE',
path: `${prefix}/${a._singular}/{aid}/${b._plural}/{scope}`, path: `${prefix}/${names.a.singular}/{aid}/${names.b.plural}/{scope}`,
@error @error
async handler(request, reply) { 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)) { const base = await a.findOne({
try { where: {
where[key] = JSON.parse(where[key]); id: request.params.aid
} catch (e) {
//
} }
} });
let list = await b.scope(request.params.scope).findAll({ const method = getMethod(base, names.b);
const list = await method({
scope: request.params.scope,
where, where,
include
include: {
model: a,
where: {
id: request.params.aid
}
}
}); });
await* list.map(instance => instance.destroy()); 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({ server.route({
method: 'PUT', method: 'PUT',
path: `${prefix}/${a._singular}/{aid}/${b._plural}`, path: `${prefix}/${names.a.singular}/{aid}/${names.b.plural}`,
@error @error
async handler(request, reply) { async handler(request, reply) {
let list = await b.findOne({ const include = parseInclude(request);
include: { const where = parseWhere(request);
model: a,
where: {
id: request.params.aid,
...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)); await* list.map(instance => instance.update(request.payload));
reply(list); reply(list);

View File

@ -1,82 +1,87 @@
import joi from 'joi'; import joi from 'joi';
import error from '../error'; import error from '../error';
import _ from 'lodash'; import _ from 'lodash';
import { parseInclude, parseWhere, getMethod } from '../utils';
let prefix; let prefix;
export default (server, a, b, options) => { export default (server, a, b, names, options) => {
prefix = options.prefix; prefix = options.prefix;
get(server, a, b); get(server, a, b, names);
create(server, a, b); create(server, a, b, names);
destroy(server, a, b); destroy(server, a, b, names);
update(server, a, b); update(server, a, b, names);
} }
export const get = (server, a, b) => { export const get = (server, a, b, names) => {
server.route({ server.route({
method: 'GET', method: 'GET',
path: `${prefix}/${a._singular}/{aid}/${b._singular}`, path: `${prefix}/${names.a.singular}/{aid}/${names.b.singular}`,
@error @error
async handler(request, reply) { async handler(request, reply) {
let include = []; const include = parseInclude(request);
if (request.query.include) const where = parseWhere(request);
include = [request.models[request.query.include]];
let where = _.omit(request.query, 'include'); const base = await a.findOne({
where: {
let [instance] = await b.findAll({ id: request.params.aid
where, }
include: include.concat({
model: a,
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({ server.route({
method: 'POST', method: 'POST',
path: `${prefix}/${a._singular}/{id}/${b._singular}`, path: `${prefix}/${names.a.singular}/{id}/${names.b.singular}`,
@error @error
async handler(request, reply) { async handler(request, reply) {
request.payload[a.name + 'Id'] = request.params.id; const base = await a.findOne({
let instance = await request.models[b.name].create(request.payload); where: {
id: request.params.id
}
});
const method = getMethod(base, names.b, false, 'create');
const instance = await method(request.payload);
reply(instance); reply(instance);
} }
}) })
} }
export const destroy = (server, a, b) => { export const destroy = (server, a, b, names) => {
server.route({ server.route({
method: 'DELETE', method: 'DELETE',
path: `${prefix}/${a._singular}/{aid}/${b._singular}/{bid}`, path: `${prefix}/${names.a.singular}/{aid}/${names.b.singular}/{bid}`,
@error @error
async handler(request, reply) { async handler(request, reply) {
let instance = await b.findOne({ const include = parseInclude(request);
where: { const where = parseWhere(request);
id: request.params.bid
},
include: [{ const base = await a.findOne({
model: a, where: {
where: { id: request.params.aid
id: request.params.aid }
}
}]
}); });
const method = getMethod(base, names.b, false);
const instance = await method({ where, include });
await instance.destroy(); await instance.destroy();
reply(instance); 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({ server.route({
method: 'PUT', method: 'PUT',
path: `${prefix}/${a._singular}/{aid}/${b._singular}/{bid}`, path: `${prefix}/${names.a.singular}/{aid}/${names.b.singular}/{bid}`,
@error @error
async handler(request, reply) { async handler(request, reply) {
let instance = await b.findOne({ const include = parseInclude(request);
where: { const where = parseWhere(request);
id: request.params.bid
},
include: [{ const base = await a.findOne({
model: a, where: {
where: { id: request.params.aid
id: request.params.aid }
}
}]
}); });
const method = getMethod(base, names.b, false);
const instance = await method({ where, include });
await instance.update(request.payload); await instance.update(request.payload);
reply(instance); reply(instance);

View File

@ -1,6 +1,7 @@
import joi from 'joi'; import joi from 'joi';
import error from './error'; import error from './error';
import _ from 'lodash'; import _ from 'lodash';
import { parseInclude, parseWhere } from './utils';
let prefix; let prefix;
@ -23,20 +24,10 @@ export const list = (server, model) => {
@error @error
async handler(request, reply) { async handler(request, reply) {
if (request.query.include) const include = parseInclude(request);
var include = [request.models[request.query.include]]; const where = parseWhere(request);
let where = _.omit(request.query, 'include'); const list = await model.findAll({
for (const key of Object.keys(where)) {
try {
where[key] = JSON.parse(where[key]);
} catch (e) {
//
}
}
let list = await model.findAll({
where, include where, include
}); });
@ -52,20 +43,10 @@ export const get = (server, model) => {
@error @error
async handler(request, reply) { async handler(request, reply) {
if (request.query.include) const include = parseInclude(request);
var include = [request.models[request.query.include]]; const where = parseWhere(request);
let where = request.params.id ? { id : request.params.id } : _.omit(request.query, 'include'); const instance = await model.findOne({ where, include });
for (const key of Object.keys(where)) {
try {
where[key] = JSON.parse(where[key]);
} catch (e) {
//
}
}
let instance = await model.findOne({ where, include });
reply(instance); reply(instance);
}, },
@ -88,20 +69,10 @@ export const scope = (server, model) => {
@error @error
async handler(request, reply) { async handler(request, reply) {
if (request.query.include) const include = parseInclude(request);
var include = [request.models[request.query.include]]; const where = parseWhere(request);
let where = _.omit(request.query, 'include'); const list = await model.scope(request.params.scope).findAll({ include, where });
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 });
reply(list); reply(list);
}, },
@ -122,7 +93,7 @@ export const create = (server, model) => {
@error @error
async handler(request, reply) { async handler(request, reply) {
let instance = await model.create(request.payload); const instance = await model.create(request.payload);
reply(instance); reply(instance);
} }
@ -136,17 +107,10 @@ export const destroy = (server, model) => {
@error @error
async handler(request, reply) { 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)) { const list = await model.findAll({ where });
try {
where[key] = JSON.parse(where[key]);
} catch (e) {
//
}
}
let list = await model.findAll({ where });
await* list.map(instance => instance.destroy()); await* list.map(instance => instance.destroy());
@ -164,18 +128,8 @@ export const destroyScope = (server, model) => {
@error @error
async handler(request, reply) { async handler(request, reply) {
if (request.query.include) const include = parseInclude(request);
var include = [request.models[request.query.include]]; 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 }); let list = await model.scope(request.params.scope).findAll({ include, where });

View File

@ -36,12 +36,25 @@ const register = (server, options = {}, next) => {
for (let key of Object.keys(model.associations)) { for (let key of Object.keys(model.associations)) {
let association = model.associations[key]; let association = model.associations[key];
let { associationType, source, target } = association; let { associationType, source, target } = association;
// console.dir(association, null, { depth: null });
let sourceName = source.options.name; let sourceName = source.options.name;
let targetName = target.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 targetAssociations = target.associations[sourceName.plural] || target.associations[sourceName.singular];
let sourceType = association.associationType, let sourceType = association.associationType,
@ -49,26 +62,26 @@ const register = (server, options = {}, next) => {
try { try {
if (sourceType === 'BelongsTo' && (targetType === 'BelongsTo' || !targetType)) { if (sourceType === 'BelongsTo' && (targetType === 'BelongsTo' || !targetType)) {
associations.oneToOne(server, source, target, options); associations.oneToOne(server, source, target, names(), options);
associations.oneToOne(server, target, source, options); associations.oneToOne(server, target, source, names(1), options);
} }
if (sourceType === 'BelongsTo' && targetType === 'HasMany') { if (sourceType === 'BelongsTo' && targetType === 'HasMany') {
associations.oneToOne(server, source, target, options); associations.oneToOne(server, source, target, names(), options);
associations.oneToOne(server, target, source, options); associations.oneToOne(server, target, source, names(1), options);
associations.oneToMany(server, target, source, options); associations.oneToMany(server, target, source, names(1), options);
} }
if (sourceType === 'BelongsToMany' && targetType === 'BelongsToMany') { if (sourceType === 'BelongsToMany' && targetType === 'BelongsToMany') {
associations.oneToOne(server, source, target, options); associations.oneToOne(server, source, target, names(), options);
associations.oneToOne(server, target, source, options); associations.oneToOne(server, target, source, names(1), options);
associations.oneToMany(server, source, target, options); associations.oneToMany(server, source, target, names(), options);
associations.oneToMany(server, target, source, options); associations.oneToMany(server, target, source, names(1), options);
} }
associations.associate(server, source, target, options); associations.associate(server, source, target, names(), options);
associations.associate(server, target, source, options); associations.associate(server, target, source, names(1), options);
} catch(e) { } catch(e) {
// There might be conflicts in case of models associated with themselves and some other // There might be conflicts in case of models associated with themselves and some other
// rare cases. // rare cases.

39
src/utils.js Normal file
View File

@ -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;
}