2016-05-18 22:50:06 +02:00
|
|
|
'use strict';
|
|
|
|
|
2018-12-17 22:49:54 +01:00
|
|
|
const _ = require('lodash')
|
|
|
|
|
|
|
|
const Constraints = require('../../shared/validation/constraints')
|
|
|
|
const ErrorTypes = require('../utils/errorTypes')
|
|
|
|
const Logger = require('../logger')
|
|
|
|
const Validator = require('../validation/validator')
|
|
|
|
|
|
|
|
function respond(res, httpCode, data, type) {
|
|
|
|
switch (type) {
|
|
|
|
case 'html':
|
|
|
|
res.writeHead(httpCode, {'Content-Type': 'text/html'});
|
|
|
|
res.end(data);
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
res.writeHead(httpCode, {'Content-Type': 'application/json'});
|
|
|
|
res.end(JSON.stringify(data));
|
|
|
|
break;
|
2016-05-18 22:50:06 +02:00
|
|
|
}
|
2018-12-17 22:49:54 +01:00
|
|
|
}
|
2016-05-18 22:50:06 +02:00
|
|
|
|
2018-12-17 22:49:54 +01:00
|
|
|
function orderByClause(restParams, defaultSortField, allowedSortFields) {
|
|
|
|
let sortField = _.includes(allowedSortFields, restParams._sortField) ? restParams._sortField : undefined;
|
|
|
|
if (!sortField) {
|
|
|
|
sortField = defaultSortField;
|
2016-06-11 12:08:50 +02:00
|
|
|
}
|
|
|
|
|
2018-12-17 22:49:54 +01:00
|
|
|
return {
|
|
|
|
query: 'ORDER BY ' + sortField + ' ' + (restParams._sortDir === 'ASC' ? 'ASC' : 'DESC'),
|
|
|
|
params: []
|
|
|
|
};
|
|
|
|
}
|
2016-06-11 12:08:50 +02:00
|
|
|
|
2018-12-17 22:49:54 +01:00
|
|
|
function limitOffsetClause(restParams) {
|
|
|
|
const page = restParams._page;
|
|
|
|
const perPage = restParams._perPage;
|
2016-06-11 12:08:50 +02:00
|
|
|
|
2018-12-17 22:49:54 +01:00
|
|
|
return {
|
|
|
|
query: 'LIMIT ? OFFSET ?',
|
|
|
|
params: [perPage, ((page - 1) * perPage)]
|
|
|
|
};
|
|
|
|
}
|
2016-06-11 12:08:50 +02:00
|
|
|
|
2018-12-17 22:49:54 +01:00
|
|
|
function escapeForLikePattern(str) {
|
|
|
|
return str
|
|
|
|
.replace(/\\/g, '\\\\')
|
|
|
|
.replace(/%/g, '\\%')
|
|
|
|
.replace(/_/g, '\\_');
|
|
|
|
}
|
2016-06-11 12:08:50 +02:00
|
|
|
|
2018-12-17 22:49:54 +01:00
|
|
|
function filterCondition(restParams, filterFields) {
|
|
|
|
if (_.isEmpty(filterFields)) {
|
2016-06-11 12:08:50 +02:00
|
|
|
return {
|
2018-12-17 22:49:54 +01:00
|
|
|
query: '1 = 1',
|
|
|
|
params: []
|
2016-06-11 12:08:50 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2018-12-17 22:49:54 +01:00
|
|
|
let query = _.join(
|
|
|
|
_.map(filterFields, function (field) {
|
|
|
|
return 'LOWER(' + field + ') LIKE ?';
|
|
|
|
}),
|
|
|
|
' OR '
|
|
|
|
);
|
|
|
|
|
|
|
|
query += ' ESCAPE \'\\\'';
|
|
|
|
|
|
|
|
const search = '%' + (_.isString(restParams.q) ? escapeForLikePattern(_.toLower(restParams.q.trim())) : '') + '%';
|
|
|
|
const params = _.times(filterFields.length, _.constant(search));
|
2016-07-23 11:30:41 +02:00
|
|
|
|
2016-05-18 22:50:06 +02:00
|
|
|
return {
|
2018-12-17 22:49:54 +01:00
|
|
|
query: query,
|
|
|
|
params: params
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function getConstrainedValues(data, constraints) {
|
|
|
|
const values = {};
|
|
|
|
_.each(_.keys(constraints), function (key) {
|
|
|
|
const value = data[key];
|
|
|
|
values[key] =
|
|
|
|
_.isUndefined(value) && !_.isUndefined(constraints[key].default) ? constraints[key].default : value;
|
|
|
|
});
|
|
|
|
return values;
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
getData (req) {
|
|
|
|
return _.extend({}, req.body, req.params, req.query);
|
|
|
|
},
|
|
|
|
|
|
|
|
getValidRestParams(type, subtype, req, callback) {
|
|
|
|
const constraints = Constraints.rest[type];
|
|
|
|
if (!_.isPlainObject(constraints)) {
|
|
|
|
Logger.tag('validation', 'rest').error('Unknown REST resource type: {}', type);
|
|
|
|
return callback({data: 'Internal error.', type: ErrorTypes.internalError});
|
|
|
|
}
|
|
|
|
|
|
|
|
let filterConstraints = {};
|
|
|
|
if (subtype) {
|
|
|
|
filterConstraints = Constraints[subtype + 'Filters'];
|
|
|
|
if (!_.isPlainObject(filterConstraints)) {
|
|
|
|
Logger.tag('validation', 'rest').error('Unknown REST resource subtype: {}', subtype);
|
2016-06-07 10:50:15 +02:00
|
|
|
return callback({data: 'Internal error.', type: ErrorTypes.internalError});
|
|
|
|
}
|
2018-12-17 22:49:54 +01:00
|
|
|
}
|
2016-06-07 10:50:15 +02:00
|
|
|
|
2018-12-17 22:49:54 +01:00
|
|
|
const data = this.getData(req);
|
2016-07-23 11:30:41 +02:00
|
|
|
|
2018-12-17 22:49:54 +01:00
|
|
|
const restParams = getConstrainedValues(data, constraints);
|
|
|
|
const filterParams = getConstrainedValues(data, filterConstraints);
|
2016-06-07 10:50:15 +02:00
|
|
|
|
2018-12-17 22:49:54 +01:00
|
|
|
const areValidParams = Validator.forConstraints(constraints);
|
|
|
|
const areValidFilters = Validator.forConstraints(filterConstraints);
|
|
|
|
if (!areValidParams(restParams) || !areValidFilters(filterParams)) {
|
|
|
|
return callback({data: 'Invalid REST parameters.', type: ErrorTypes.badRequest});
|
|
|
|
}
|
2016-06-07 10:50:15 +02:00
|
|
|
|
2018-12-17 22:49:54 +01:00
|
|
|
restParams.filters = filterParams;
|
2016-06-07 10:50:15 +02:00
|
|
|
|
2018-12-17 22:49:54 +01:00
|
|
|
callback(null, restParams);
|
|
|
|
},
|
2016-07-23 11:30:41 +02:00
|
|
|
|
2018-12-17 22:49:54 +01:00
|
|
|
filter (entities, allowedFilterFields, restParams) {
|
|
|
|
let query = restParams.q;
|
|
|
|
if (query) {
|
|
|
|
query = _.toLower(query.trim());
|
|
|
|
}
|
2016-06-07 10:50:15 +02:00
|
|
|
|
2018-12-17 22:49:54 +01:00
|
|
|
function queryMatches(entity) {
|
|
|
|
if (!query) {
|
|
|
|
return true;
|
2016-06-07 12:39:52 +02:00
|
|
|
}
|
2018-12-17 22:49:54 +01:00
|
|
|
return _.some(allowedFilterFields, function (field) {
|
|
|
|
let value = entity[field];
|
|
|
|
if (_.isNumber(value)) {
|
|
|
|
value = value.toString();
|
2016-07-23 11:30:41 +02:00
|
|
|
}
|
|
|
|
|
2018-12-17 22:49:54 +01:00
|
|
|
if (!_.isString(value) || _.isEmpty(value)) {
|
|
|
|
return false;
|
|
|
|
}
|
2016-07-23 11:30:41 +02:00
|
|
|
|
2018-12-17 22:49:54 +01:00
|
|
|
value = _.toLower(value);
|
|
|
|
if (field === 'mac') {
|
|
|
|
return _.includes(value.replace(/:/g, ''), query.replace(/:/g, ''));
|
2016-07-23 11:30:41 +02:00
|
|
|
}
|
|
|
|
|
2018-12-17 22:49:54 +01:00
|
|
|
return _.includes(value, query);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
const filters = restParams.filters;
|
|
|
|
|
|
|
|
function filtersMatch(entity) {
|
|
|
|
if (_.isEmpty(filters)) {
|
|
|
|
return true;
|
2016-07-23 11:30:41 +02:00
|
|
|
}
|
|
|
|
|
2018-12-17 22:49:54 +01:00
|
|
|
return _.every(filters, function (value, key) {
|
|
|
|
if (_.isUndefined(value)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (_.startsWith(key, 'has')) {
|
|
|
|
const entityKey = key.substr(3, 1).toLowerCase() + key.substr(4);
|
|
|
|
return _.isEmpty(entity[entityKey]).toString() !== value;
|
|
|
|
}
|
|
|
|
return entity[key] === value;
|
2016-06-07 12:39:52 +02:00
|
|
|
});
|
2018-12-17 22:49:54 +01:00
|
|
|
}
|
2016-06-07 12:39:52 +02:00
|
|
|
|
2018-12-17 22:49:54 +01:00
|
|
|
return _.filter(entities, function (entity) {
|
|
|
|
return queryMatches(entity) && filtersMatch(entity);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
sort (entities, allowedSortFields, restParams) {
|
|
|
|
const sortField = _.includes(allowedSortFields, restParams._sortField) ? restParams._sortField : undefined;
|
|
|
|
if (!sortField) {
|
|
|
|
return entities;
|
|
|
|
}
|
2016-06-07 11:58:29 +02:00
|
|
|
|
2018-12-17 22:49:54 +01:00
|
|
|
const sorted = _.sortBy(entities, [sortField]);
|
2016-06-07 11:58:29 +02:00
|
|
|
|
2018-12-17 22:49:54 +01:00
|
|
|
return restParams._sortDir === 'ASC' ? sorted : _.reverse(sorted);
|
|
|
|
},
|
2016-06-07 11:58:29 +02:00
|
|
|
|
2018-12-17 22:49:54 +01:00
|
|
|
getPageEntities (entities, restParams) {
|
|
|
|
const page = restParams._page;
|
|
|
|
const perPage = restParams._perPage;
|
2016-06-07 11:58:29 +02:00
|
|
|
|
2018-12-17 22:49:54 +01:00
|
|
|
return entities.slice((page - 1) * perPage, page * perPage);
|
|
|
|
},
|
2016-06-07 11:58:29 +02:00
|
|
|
|
2018-12-17 22:49:54 +01:00
|
|
|
whereCondition: filterCondition,
|
2016-06-26 13:06:58 +02:00
|
|
|
|
2018-12-17 22:49:54 +01:00
|
|
|
filterClause (restParams, defaultSortField, allowedSortFields, filterFields) {
|
|
|
|
const orderBy = orderByClause(
|
|
|
|
restParams,
|
|
|
|
defaultSortField,
|
|
|
|
allowedSortFields
|
|
|
|
);
|
|
|
|
const limitOffset = limitOffsetClause(restParams);
|
2016-06-11 12:08:50 +02:00
|
|
|
|
2018-12-17 22:49:54 +01:00
|
|
|
const filter = filterCondition(
|
|
|
|
restParams,
|
|
|
|
filterFields
|
|
|
|
);
|
2016-06-11 12:08:50 +02:00
|
|
|
|
2018-12-17 22:49:54 +01:00
|
|
|
return {
|
|
|
|
query: filter.query + ' ' + orderBy.query + ' ' + limitOffset.query,
|
|
|
|
params: _.concat(filter.params, orderBy.params, limitOffset.params)
|
|
|
|
};
|
|
|
|
},
|
2016-06-11 12:08:50 +02:00
|
|
|
|
2018-12-17 22:49:54 +01:00
|
|
|
success (res, data) {
|
|
|
|
respond(res, 200, data, 'json');
|
|
|
|
},
|
2016-06-07 14:08:04 +02:00
|
|
|
|
2018-12-17 22:49:54 +01:00
|
|
|
successHtml (res, html) {
|
|
|
|
respond(res, 200, html, 'html');
|
|
|
|
},
|
2016-05-18 22:50:06 +02:00
|
|
|
|
2018-12-17 22:49:54 +01:00
|
|
|
error (res, err) {
|
|
|
|
respond(res, err.type.code, err.data, 'json');
|
|
|
|
}
|
|
|
|
}
|