mailtrain/server/lib/dt-helpers.js
2019-01-04 21:31:01 +01:00

200 lines
7.7 KiB
JavaScript

'use strict';
const knex = require('./knex');
const entitySettings = require('./entity-settings');
const { enforce } = require('./helpers');
const shares = require('../models/shares');
async function ajaxListTx(tx, params, queryFun, columns, options) {
options = options || {};
const columnsNames = [];
const columnsSelect = [];
for (const col of columns) {
if (typeof col === 'string') {
columnsNames.push(col);
columnsSelect.push(col);
} else {
columnsNames.push(col.name);
if (col.raw) {
columnsSelect.push(tx.raw(col.raw, col.data || []));
} else if (col.query) {
columnsSelect.push(function () { return col.query(this); });
}
}
}
if (params.operation === 'getBy') {
const query = queryFun(tx);
query.whereIn(columnsNames[parseInt(params.column)], params.values);
query.select(columnsSelect);
query.options({nestTables: '.'});
const rows = await query;
const rowsOfArray = rows.map(row => Object.keys(row).map(key => row[key]));
return rowsOfArray;
} else {
const whereFun = function() {
let searchVal = '%' + params.search.value.replace(/\\/g, '\\\\').replace(/([%_])/g, '\\$1') + '%';
for (let colIdx = 0; colIdx < params.columns.length; colIdx++) {
const col = params.columns[colIdx];
if (col.searchable) {
this.orWhere(columnsNames[parseInt(col.data)], 'like', searchVal);
}
}
}
/* There are a few SQL peculiarities that make this query a bit weird:
- Group by (which is used in getting permissions) don't go well with count(*). Thus we run the actual query
as a sub-query and then count the number of results.
- SQL does not like if it have columns with the same name in the subquery. This happens multiple tables are joined.
To circumvent this, we select only the first column (whatever it is). Since this is not "distinct", it is supposed
to give us the right number of rows anyway.
*/
const recordsTotalQuery = tx.count('* as recordsTotal').from(function () { return queryFun(this).select(columnsSelect[0]).as('records'); }).first();
const recordsTotal = (await recordsTotalQuery).recordsTotal;
const recordsFilteredQuery = tx.count('* as recordsFiltered').from(function () { return queryFun(this).select(columnsSelect[0]).where(whereFun).as('records'); }).first();
const recordsFiltered = (await recordsFilteredQuery).recordsFiltered;
const query = queryFun(tx);
query.where(whereFun);
query.offset(parseInt(params.start));
const limit = parseInt(params.length);
if (limit >= 0) {
query.limit(limit);
}
query.select([...columnsSelect, ...options.extraColumns || [] ]);
for (const order of params.order) {
if (options.orderByBuilder) {
options.orderByBuilder(query, columnsNames[params.columns[order.column].data], order.dir);
} else {
query.orderBy(columnsNames[params.columns[order.column].data], order.dir);
}
}
query.options({nestTables: '.'});
const rows = await query;
// Here we rely on the fact that Object.keys(row) returns the columns in the same order as they are given in the select (i.e. in columnsNames).
// This should work because ES2015 guarantees chronological order of keys in an object and mysql (https://github.com/mysqljs/mysql/blob/ad014c82b2cbaf47acae1cc39e5533d3cb6eb882/lib/protocol/packets/RowDataPacket.js#L43)
// adds them in the order of select columns.
const rowsOfArray = rows.map(row => {
const arr = Object.keys(row).map(field => row[field]);
if (options.mapFun) {
const result = options.mapFun(arr);
return result || arr;
} else {
return arr;
}
});
const result = {
draw: params.draw,
recordsTotal,
recordsFiltered,
data: rowsOfArray
};
return result;
}
}
async function ajaxListWithPermissionsTx(tx, context, fetchSpecs, params, queryFun, columns, options) {
enforce(!context.user.admin, 'ajaxListWithPermissionsTx is not supposed to be called by assumed admin');
options = options || {};
const permCols = [];
for (const fetchSpec of fetchSpecs) {
const entityType = entitySettings.getEntityType(fetchSpec.entityTypeId);
const entityIdColumn = fetchSpec.column ? fetchSpec.column : entityType.entitiesTable + '.id';
permCols.push({
name: `permissions_${fetchSpec.entityTypeId}`,
query: builder => builder
.from(entityType.permissionsTable)
.select(knex.raw('GROUP_CONCAT(operation SEPARATOR \';\')'))
.whereRaw(`${entityType.permissionsTable}.entity = ${entityIdColumn}`)
.where(`${entityType.permissionsTable}.user`, context.user.id)
.as(`permissions_${fetchSpec.entityTypeId}`)
});
}
return await ajaxListTx(
tx,
params,
builder => {
let query = queryFun(builder);
for (const fetchSpec of fetchSpecs) {
const entityType = entitySettings.getEntityType(fetchSpec.entityTypeId);
if (fetchSpec.requiredOperations) {
const requiredOperations = shares.filterPermissionsByRestrictedAccessHandler(context, fetchSpec.entityTypeId, null, fetchSpec.requiredOperations, 'ajaxListWithPermissionsTx');
const entityIdColumn = fetchSpec.column ? fetchSpec.column : entityType.entitiesTable + '.id';
if (requiredOperations.length > 0) {
query = query.innerJoin(
function () {
return this.from(entityType.permissionsTable).distinct('entity').where('user', context.user.id).whereIn('operation', requiredOperations).as(`permitted__${fetchSpec.entityTypeId}`);
},
`permitted__${fetchSpec.entityTypeId}.entity`, entityIdColumn)
} else {
query = query.whereRaw('FALSE');
}
}
}
return query;
},
[
...columns,
...permCols
],
{
mapFun: data => {
for (let idx = 0; idx < fetchSpecs.length; idx++) {
data[columns.length + idx] = data[columns.length + idx].split(';');
}
if (options.mapFun) {
const result = options.mapFun(data);
return result || data;
} else {
return data;
}
},
orderByBuilder: options.orderByBuilder,
extraColumns: options.extraColumns
}
);
}
async function ajaxList(params, queryFun, columns, options) {
return await knex.transaction(async tx => {
return await ajaxListTx(tx, params, queryFun, columns, options)
});
}
async function ajaxListWithPermissions(context, fetchSpecs, params, queryFun, columns, options) {
return await knex.transaction(async tx => {
return await ajaxListWithPermissionsTx(tx, context, fetchSpecs, params, queryFun, columns, options)
});
}
module.exports.ajaxListTx = ajaxListTx;
module.exports.ajaxList = ajaxList;
module.exports.ajaxListWithPermissionsTx = ajaxListWithPermissionsTx;
module.exports.ajaxListWithPermissions = ajaxListWithPermissions;