app/src/services/query.service.js
const logger = require('logger');
const Sql2json = require('sql2json').sql2json;
const Json2sql = require('sql2json').json2sql;
const { RWAPIMicroservice } = require('rw-api-microservice-node');
const JSONAPIDeserializer = require('jsonapi-serializer').Deserializer;
const ValidationError = require('errors/validation.error');
const QueryParsingError = require('errors/queryParsing.error');
const endpoints = require('services/endpoints');
const deserializer = (obj) => new Promise((resolve, reject) => {
new JSONAPIDeserializer({
keyForAttribute: 'camelCase'
}).deserialize(obj, (err, data) => {
if (err) {
reject(err);
return;
}
resolve(data);
});
});
const containApps = (apps1, apps2) => {
if (!apps1 || !apps2) {
return false;
}
for (let i = 0, { length } = apps1; i < length; i++) {
for (let j = 0, length2 = apps2.length; j < length2; j++) {
if (apps1[i] === apps2[j]) {
return true;
}
}
}
return false;
};
const checkUserHasPermission = (user, dataset) => {
if (user && dataset) {
// check if user is admin of any application of the dataset or manager and owner of the dataset
if (user.role === 'MANAGER' && user.id === dataset.userId) {
return true;
}
if (user.role === 'ADMIN' && containApps(dataset.application, user.extraUserData ? user.extraUserData.apps : null)) {
return true;
}
}
return false;
};
const generateTemplateString = (function () {
const cache = {};
function generateTemplate(template) {
let fn = cache[template];
if (!fn) {
// Replace ${expressions} (etc) with ${map.expressions}.
const sanitized = template
// eslint-disable-next-line no-useless-escape
.replace(/\$\{([\s]*[^;\s\{]+[\s]*)\}/g, (_, match) => `\$\{map.${match.trim()}\}`)
// Afterwards, replace anything that's not ${map.expressions}' (etc) with a blank string.
.replace(/(\$\{(?!map\.)[^}]+\})/g, '');
// eslint-disable-next-line no-new-func
fn = Function('map', `return \`${sanitized}\``);
}
return fn;
}
return generateTemplate;
}());
class QueryService {
static checkRoles(dataset, loggedUser) {
if (checkUserHasPermission(loggedUser, dataset)) {
return true;
}
return false;
}
static isFunction(parsed) {
logger.debug('Checking if it is a function', parsed);
if (!parsed.select || parsed.select.length !== 1 || parsed.select[0].type !== 'function') {
throw new ValidationError(400, 'Invalid query. Function not found or not supported');
}
const node = parsed.select[0];
const endpointConfig = endpoints[node.value.toLowerCase()];
if (!endpointConfig) {
throw new ValidationError(400, 'Invalid query. Function not found or not supported');
}
if (endpointConfig.arguments.length < node.arguments.length) {
throw new ValidationError(400, `Invalid query. Invalid num arguments. Max num arguments: ${endpointConfig.arguments.length}`);
}
const path = {};
const qs = {};
const body = {};
for (let i = 0, { length } = endpointConfig.arguments; i < length; i++) {
const arg = endpointConfig.arguments[i];
const argFun = node.arguments[i];
if (!argFun) {
if (arg.required) {
throw new ValidationError(400, 'Invalid query. Required param');
}
} else if (arg.location === 'path') {
if (!path[arg.name]) {
path[arg.name] = argFun.value;
} else {
path[arg.name] += `,${argFun.value}`;
}
} else if (arg.location === 'query') {
if (!qs[arg.name]) {
qs[arg.name] = argFun.value;
} else {
qs[arg.name] += `,${argFun.value}`;
}
} else if (arg.location === 'body') {
if (!body[arg.name]) {
body[arg.name] = argFun.value;
} else {
body[arg.name] += `,${argFun.value}`;
}
}
}
const uri = `${process.env.GATEWAY_URL}/v1${generateTemplateString(endpointConfig.uri)(path)}`;
return {
uri,
method: endpointConfig.method,
qs,
body,
simple: false,
resolveWithFullResponse: true,
json: true
};
}
static async isQuery(tableName, parsed, sql, ctx, endpoint) {
logger.debug('Is query');
let datasetId = ctx.params.dataset || tableName;
if (parsed.from && !datasetId) {
datasetId = parsed.from.replace(/"/g, '').replace(/'/g, '');
}
if (!datasetId) {
throw new ValidationError(400, 'Invalid query');
}
logger.debug('Obtaining dataset');
let dataset = null;
try {
logger.debug('Obtaining dataset with id/slug', datasetId);
dataset = await RWAPIMicroservice.requestToMicroservice({
uri: `/v1/dataset/${datasetId}`,
json: true,
method: 'GET',
headers: {
'x-api-key': ctx.request.headers['x-api-key'],
}
});
logger.debug('Dataset obtained correctly', dataset);
dataset = await deserializer(dataset);
} catch (err) {
if (err.statusCode === 404) {
logger.debug('Dataset not found: ', err);
throw new ValidationError(404, 'Dataset not found');
}
logger.error('Error obtaining dataset', err);
throw new ValidationError(500, err.message);
}
if (parsed && parsed.delete) {
if (!QueryService.checkRoles(dataset, ctx.request.body.loggedUser)) {
throw new ValidationError(403, 'Not authorized to execute DELETE query');
}
}
let body = null;
let qs;
if (sql) {
parsed.from = dataset.tableName;
const newSql = Json2sql.toSQL(parsed);
if (ctx.method === 'GET') {
qs = { ...ctx.query, sql: newSql };
} else {
qs = { ...ctx.query };
delete qs.sql;
body = { ...ctx.request.body, sql: newSql };
}
} else {
const newTableName = dataset.tableName;
if (ctx.method === 'GET') {
qs = { ...ctx.query, tableName: newTableName };
} else {
qs = { ...ctx.query };
body = { ...ctx.request.body, tableName: newTableName };
}
}
logger.debug('isQuery - ctx.path', ctx.path);
const options = {
uri: `${process.env.GATEWAY_URL}/v1/${endpoint}/${dataset.provider}/${dataset.id}`,
simple: false,
resolveWithFullResponse: true,
json: true,
headers: {
'x-api-key': ctx.request.headers['x-api-key'],
}
};
delete qs.loggedUser;
options.qs = qs;
if (ctx.method === 'GET') {
options.method = 'GET';
} else {
delete body.loggedUser;
options.body = body;
options.method = 'POST';
}
return options;
}
static async getTargetQuery(ctx, endpoint) {
logger.debug('Obtaining target query');
const sql = ctx.query.sql || ctx.request.body.sql;
const tableName = ctx.query.tableName || ctx.request.body.tableName;
let parsed = null;
if (!sql && !tableName) {
throw new ValidationError(400, 'Sql o FS required');
}
try {
if (sql) {
parsed = new Sql2json(sql).toJSON();
}
} catch (e) {
throw new QueryParsingError(e.message);
}
if (!tableName && !parsed.from) {
logger.debug('Check if it is a function');
return QueryService.isFunction(parsed);
}
return QueryService.isQuery(tableName, parsed, sql, ctx, endpoint);
}
static getFieldsOfSql(ctx) {
logger.debug('Obtaining fields');
const sql = ctx.query.sql || ctx.request.body.sql;
if (sql) {
const parsed = new Sql2json(sql).toJSON();
return parsed.select;
}
return null;
}
}
module.exports = QueryService;