'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(id) { const entity = await knex('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(); if (!entity) { throw new interoperableErrors.NotFoundError(); } entity.user_fields = JSON.parse(entity.user_fields); entity.params = JSON.parse(entity.params); return entity; } async function listDTAjax(params) { return await dtHelpers.ajaxList( params, tx => tx('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(entity) { let id; await knex.transaction(async tx => { await namespaceHelpers.validateEntity(tx, entity); if (!await tx('report_templates').select(['id']).where('id', entity.report_template).first()) { throw new interoperableErrors.DependencyNotFoundError(); } entity.params = JSON.stringify(entity.params); id = await tx('reports').insert(filterObject(entity, allowedKeys)); await shares.rebuildPermissions(tx, { entityTypeId: 'report', entityId: id }); }); const reportProcessor = require('../lib/report-processor'); await reportProcessor.start(id); return id; } async function updateWithConsistencyCheck(entity) { await knex.transaction(async tx => { const existing = await tx('reports').where('id', entity.id).first(); if (!entity) { throw new interoperableErrors.NotFoundError(); } existing.params = JSON.parse(existing.params); const existingHash = hash(existing); if (existingHash != entity.originalHash) { throw new interoperableErrors.ChangedError(); } if (!await tx('report_templates').select(['id']).where('id', entity.report_template).first()) { throw new interoperableErrors.DependencyNotFoundError(); } await namespaceHelpers.validateEntity(tx, entity); 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.rebuildPermissions(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 remove(id) { await knex('reports').where('id', id).del(); } 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' }; function customFieldName(id) { return id.replace(/MERGE_/, 'CUSTOM_').toLowerCase(); } async function getCampaignResults(campaign, select, extra) { const fieldList = await fields.list(campaign.list); const fieldsMapping = fieldList.reduce((map, field) => { /* Dropdowns and checkboxes are aggregated. As such, they have field.column == null and the options are in field.options. TODO - For the time being, we ignore groupped fields. */ if (field.column) { map[customFieldName(field.key)] = 'subscribers.' + field.column; } return map; }, Object.assign({}, campaignFieldsMapping)); 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, updateFields, listByState, bulkChangeState, getCampaignResults };