'use strict'; const knex = require('../lib/knex'); const hasher = require('node-object-hash')(); const { enforce, filterObject } = require('../lib/helpers'); const dtHelpers = require('../lib/dt-helpers'); const interoperableErrors = require('../shared/interoperable-errors'); const fields = require('./fields'); const namespaceHelpers = require('../lib/namespace-helpers'); const shares = require('./shares'); const ReportState = require('../shared/reports').ReportState; const allowedKeys = new Set(['name', 'description', 'report_template', 'params', 'namespace']); function hash(entity) { return hasher.hash(filterObject(entity, allowedKeys)); } async function getByIdWithTemplate(context, id) { return await knex.transaction(async tx => { await shares.enforceEntityPermissionTx(tx, context, 'report', id, 'view'); const entity = await tx('reports') .where('reports.id', id) .innerJoin('report_templates', 'reports.report_template', 'report_templates.id') .select(['reports.id', 'reports.name', 'reports.description', 'reports.report_template', 'reports.params', 'reports.state', 'reports.namespace', 'report_templates.user_fields', 'report_templates.mime_type', 'report_templates.hbs', 'report_templates.js']) .first(); entity.user_fields = JSON.parse(entity.user_fields); entity.params = JSON.parse(entity.params); entity.permissions = await shares.getPermissionsTx(tx, context, 'report', id); return entity; }); } async function listDTAjax(context, params) { return await dtHelpers.ajaxListWithPermissions( context, [ { entityTypeId: 'report', requiredOperations: ['view'] }, { entityTypeId: 'reportTemplate', requiredOperations: ['view'] } ], params, builder => builder.from('reports') .innerJoin('report_templates', 'reports.report_template', 'report_templates.id') .innerJoin('namespaces', 'namespaces.id', 'reports.namespace'), [ 'reports.id', 'reports.name', 'report_templates.name', 'reports.description', 'reports.last_run', 'namespaces.name', 'reports.state', 'report_templates.mime_type' ] ); } async function create(context, entity) { let id; await knex.transaction(async tx => { await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createReport'); await shares.enforceEntityPermissionTx(tx, context, 'reportTemplate', entity.report_template, 'execute'); await namespaceHelpers.validateEntity(tx, entity); entity.params = JSON.stringify(entity.params); const ids = await tx('reports').insert(filterObject(entity, allowedKeys)); id = ids[0]; await shares.rebuildPermissionsTx(tx, { entityTypeId: 'report', entityId: id }); }); const reportProcessor = require('../lib/report-processor'); await reportProcessor.start(id); return id; } async function updateWithConsistencyCheck(context, entity) { await knex.transaction(async tx => { await shares.enforceEntityPermissionTx(tx, context, 'report', entity.id, 'edit'); await shares.enforceEntityPermissionTx(tx, context, 'reportTemplate', entity.report_template, 'execute'); const existing = await tx('reports').where('id', entity.id).first(); if (!existing) { throw new interoperableErrors.NotFoundError(); } existing.params = JSON.parse(existing.params); const existingHash = hash(existing); if (existingHash !== entity.originalHash) { throw new interoperableErrors.ChangedError(); } await namespaceHelpers.validateEntity(tx, entity); await namespaceHelpers.validateMove(context, entity, existing, 'report', 'createReport', 'delete'); entity.params = JSON.stringify(entity.params); const filteredUpdates = filterObject(entity, allowedKeys); filteredUpdates.state = ReportState.SCHEDULED; await tx('reports').where('id', entity.id).update(filteredUpdates); await shares.rebuildPermissionsTx(tx, { entityTypeId: 'report', entityId: entity.id }); }); // This require is here to avoid cyclic dependency const reportProcessor = require('../lib/report-processor'); await reportProcessor.start(entity.id); } async function removeTx(tx, context, id) { await shares.enforceEntityPermissionTx(tx, context, 'report', id, 'delete'); await tx('reports').where('id', id).del(); // FIXME: Remove generated files } async function remove(context, id) { await knex.transaction(async tx => { await removeTx(tx, context, id); }); } async function removeAllByReportTemplateIdTx(tx, context, templateId) { const entities = await tx('reports').where('report_template', templateId).select(['id']); for (const entity of entities) { await removeTx(tx, context, entity.id); } } async function updateFields(id, fields) { return await knex('reports').where('id', id).update(fields); } async function listByState(state, limit) { return await knex('reports').where('state', state).limit(limit); } async function bulkChangeState(oldState, newState) { return await knex('reports').where('state', oldState).update('state', newState); } const campaignFieldsMapping = { tracker_count: 'tracker.count', country: 'tracker.country', device_type: 'tracker.device_type', status: 'campaign.status', first_name: 'subscribers.first_name', last_name: 'subscribers.last_name', email: 'subscribers.email' }; async function getCampaignResults(context, campaign, select, extra) { const flds = await fields.list(context, campaign.list); const fieldsMapping = Object.assign({}, campaignFieldsMapping); for (const fld of flds) { /* Dropdown and checkbox groups have field.column == null TODO - For the time being, we don't group options and we don't expand enums. We just provide it as it is in the DB. */ if (fld.column) { fieldsMapping[fld.key.toLowerCase()] = 'subscribers.' + fld.column; } } let selFields = []; for (let idx = 0; idx < select.length; idx++) { const item = select[idx]; if (item in fieldsMapping) { selFields.push(fieldsMapping[item] + ' AS ' + item); } else if (item === '*') { selFields = selFields.concat(Object.keys(fieldsMapping).map(item => fieldsMapping[item] + ' AS ' + item)); } else { selFields.push(item); } } let query = knex(`subscription__${campaign.list} AS subscribers`) .innerJoin(`campaign__${campaign.id} AS campaign`, 'subscribers.id', 'campaign.subscription') .leftJoin(`campaign_tracker__${campaign.id} AS tracker`, 'subscribers.id', 'tracker.subscriber') .select(selFields); if (extra) { query = extra(query); } return await query; } module.exports = { ReportState, hash, getByIdWithTemplate, listDTAjax, create, updateWithConsistencyCheck, remove, removeAllByReportTemplateIdTx, updateFields, listByState, bulkChangeState, getCampaignResults, };