200 lines
7.7 KiB
JavaScript
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;
|