mailtrain/server/models/lists.js
Tomas Bures 2fe7f82be3 Merge and cleanup of PR #564
The namespace filter in campaigns was dropped (i.e. "Work with campaign's namespace"). Instead, we need a universal solution. For instance a namespace slector somewhere in the top-right corner, which should apply to everything (not just campaigns).

Nevertheless, I kept the ...-by-namespace rest endpoints and related functions in models because they will be useful for implementing the universal namespace selection feature.
2019-03-27 00:41:18 +01:00

286 lines
11 KiB
JavaScript

'use strict';
const knex = require('../lib/knex');
const hasher = require('node-object-hash')();
const dtHelpers = require('../lib/dt-helpers');
const shortid = require('shortid');
const { enforce, filterObject } = require('../lib/helpers');
const interoperableErrors = require('../../shared/interoperable-errors');
const shares = require('./shares');
const namespaceHelpers = require('../lib/namespace-helpers');
const fields = require('./fields');
const segments = require('./segments');
const imports = require('./imports');
const entitySettings = require('../lib/entity-settings');
const dependencyHelpers = require('../lib/dependency-helpers');
const {EntityActivityType} = require('../../shared/activity-log');
const activityLog = require('../lib/activity-log');
const {UnsubscriptionMode, FieldWizard} = require('../../shared/lists');
const allowedKeys = new Set(['name', 'description', 'default_form', 'public_subscribe', 'unsubscription_mode', 'contact_email', 'homepage', 'namespace', 'to_name', 'listunsubscribe_disabled', 'send_configuration']);
function hash(entity) {
return hasher.hash(filterObject(entity, allowedKeys));
}
async function _listDTAjax(context, namespaceId, params) {
const campaignEntityType = entitySettings.getEntityType('campaign');
return await dtHelpers.ajaxListWithPermissions(
context,
[{ entityTypeId: 'list', requiredOperations: ['view'] }],
params,
builder => {
builder = builder
.from('lists')
.innerJoin('namespaces', 'namespaces.id', 'lists.namespace');
if (namespaceId) {
builder = builder.where('lists.namespace', namespaceId);
}
return builder;
},
['lists.id', 'lists.name', 'lists.cid', 'lists.subscribers', 'lists.description', 'namespaces.name',
{
name: 'triggerCount',
query: builder =>
builder.from('campaigns')
.innerJoin('campaign_lists', 'campaigns.id', 'campaign_lists.campaign')
.innerJoin('triggers', 'campaigns.id', 'triggers.campaign')
.innerJoin(campaignEntityType.permissionsTable, 'campaigns.id', `${campaignEntityType.permissionsTable}.entity`)
.whereRaw('campaign_lists.list = lists.id')
.where(`${campaignEntityType.permissionsTable}.operation`, 'viewTriggers')
.count()
.as('triggerCount')
}
]
);
}
async function listDTAjax(context, params) {
return await _listDTAjax(context, undefined, params);
}
async function listByNamespaceDTAjax(context, namespaceId, params) {
return await _listDTAjax(context, namespaceId, params);
}
async function listWithSegmentByCampaignDTAjax(context, campaignId, params) {
return await dtHelpers.ajaxListWithPermissions(
context,
[{ entityTypeId: 'list', requiredOperations: ['view'] }],
params,
builder => builder
.from('lists')
.innerJoin('campaign_lists', 'campaign_lists.list', 'lists.id')
.leftJoin('segments', 'segments.id', 'campaign_lists.segment')
.innerJoin('namespaces', 'namespaces.id', 'lists.namespace')
.where('campaign_lists.campaign', campaignId)
.orderBy('campaign_lists.id', 'asc'),
['lists.id', 'lists.name', 'lists.cid', 'namespaces.name', 'segments.name']
);
}
async function getByIdTx(tx, context, id) {
await shares.enforceEntityPermissionTx(tx, context, 'list', id, 'view');
const entity = await tx('lists').where('id', id).first();
return entity;
}
async function getById(context, id) {
return await knex.transaction(async tx => {
// note that permissions are not obtained here as this methods is used only with synthetic admin context
return await getByIdTx(tx, context, id);
});
}
async function getByIdWithListFields(context, id) {
return await knex.transaction(async tx => {
const entity = await getByIdTx(tx, context, id);
entity.permissions = await shares.getPermissionsTx(tx, context, 'list', id);
entity.listFields = await fields.listByOrderListTx(tx, id);
return entity;
});
}
async function getByCidTx(tx, context, cid) {
const entity = await tx('lists').where('cid', cid).first();
if (!entity) {
shares.throwPermissionDenied();
}
await shares.enforceEntityPermissionTx(tx, context, 'list', entity.id, 'view');
return entity;
}
async function getByCid(context, cid) {
return await knex.transaction(async tx => {
return getByCidTx(tx, context, cid);
});
}
async function _validateAndPreprocess(tx, entity) {
await namespaceHelpers.validateEntity(tx, entity);
enforce(entity.unsubscription_mode >= UnsubscriptionMode.MIN && entity.unsubscription_mode <= UnsubscriptionMode.MAX, 'Unknown unsubscription mode');
}
async function create(context, entity) {
return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createList');
await _validateAndPreprocess(tx, entity);
const fieldsToAdd = [];
const fieldWizard = entity.fieldWizard;
if (fieldWizard === FieldWizard.FIRST_LAST_NAME) {
if (entity.to_name === null) {
entity.to_name = '[MERGE_FIRST_NAME] [MERGE_LAST_NAME]';
}
fieldsToAdd.push({
name: 'First name',
key: 'MERGE_FIRST_NAME',
default_value: '',
type: 'text',
group: null,
settings: {}
});
fieldsToAdd.push({
name: 'Last name',
key: 'MERGE_LAST_NAME',
default_value: '',
type: 'text',
group: null,
settings: {}
});
} else if (fieldWizard === FieldWizard.NAME) {
if (entity.to_name === null) {
entity.to_name = '[MERGE_NAME]';
}
fieldsToAdd.push({
name: 'Name',
key: 'MERGE_NAME',
default_value: '',
type: 'text',
group: null,
settings: {}
});
}
const filteredEntity = filterObject(entity, allowedKeys);
filteredEntity.cid = shortid.generate();
const ids = await tx('lists').insert(filteredEntity);
const id = ids[0];
await knex.schema.raw('CREATE TABLE `subscription__' + id + '` (\n' +
' `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n' +
' `cid` varchar(255) CHARACTER SET ascii NOT NULL,\n' +
' `email` varchar(255) CHARACTER SET utf8 DEFAULT NULL,\n' +
' `hash_email` varchar(255) CHARACTER SET ascii NOT NULL,\n' +
' `source_email` int(11) DEFAULT NULL,\n' + // Altough this is a reference to an import, it is represented as signed int(11). This is because we use negative values for constant from SubscriptionSource
' `opt_in_ip` varchar(100) DEFAULT NULL,\n' +
' `opt_in_country` varchar(2) DEFAULT NULL,\n' +
' `tz` varchar(100) CHARACTER SET ascii DEFAULT NULL,\n' +
' `status` tinyint(4) unsigned NOT NULL DEFAULT \'1\',\n' +
' `is_test` tinyint(4) unsigned NOT NULL DEFAULT \'0\',\n' +
' `status_change` timestamp NULL DEFAULT NULL,\n' +
' `unsubscribed` timestamp NULL DEFAULT NULL,\n' +
' `latest_open` timestamp NULL DEFAULT NULL,\n' +
' `latest_click` timestamp NULL DEFAULT NULL,\n' +
' `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,\n' +
' PRIMARY KEY (`id`),\n' +
' UNIQUE KEY `hash_email` (`hash_email`),\n' +
' UNIQUE KEY `cid` (`cid`),\n' +
' KEY `email` (`email`),\n' +
' KEY `status` (`status`),\n' +
' KEY `subscriber_tz` (`tz`),\n' +
' KEY `is_test` (`is_test`),\n' +
' KEY `latest_open` (`latest_open`),\n' +
' KEY `latest_click` (`latest_click`),\n' +
' KEY `created` (`created`)\n' +
') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n');
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'list', entityId: id });
for (const fld of fieldsToAdd) {
await fields.createTx(tx, context, id, fld);
}
await activityLog.logEntityActivity('list', EntityActivityType.CREATE, id);
return id;
});
}
async function updateWithConsistencyCheck(context, entity) {
await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'list', entity.id, 'edit');
const existing = await tx('lists').where('id', entity.id).first();
if (!existing) {
throw new interoperableErrors.NotFoundError();
}
const existingHash = hash(existing);
if (existingHash !== entity.originalHash) {
throw new interoperableErrors.ChangedError();
}
await _validateAndPreprocess(tx, entity);
await namespaceHelpers.validateMove(context, entity, existing, 'list', 'createList', 'delete');
await tx('lists').where('id', entity.id).update(filterObject(entity, allowedKeys));
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'list', entityId: entity.id });
await activityLog.logEntityActivity('list', EntityActivityType.UPDATE, entity.id);
});
}
async function remove(context, id) {
await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'list', id, 'delete');
await dependencyHelpers.ensureNoDependencies(tx, context, id, [
{
entityTypeId: 'campaign',
query: tx => tx('campaign_lists')
.where('campaign_lists.list', id)
.innerJoin('campaigns', 'campaign_lists.campaign', 'campaigns.id')
.select(['campaigns.id', 'campaigns.name'])
}
]);
await fields.removeAllByListIdTx(tx, context, id);
await segments.removeAllByListIdTx(tx, context, id);
await imports.removeAllByListIdTx(tx, context, id);
await tx('lists').where('id', id).del();
await knex.schema.dropTableIfExists('subscription__' + id);
await activityLog.logEntityActivity('list', EntityActivityType.REMOVE, id);
});
}
module.exports.UnsubscriptionMode = UnsubscriptionMode;
module.exports.hash = hash;
module.exports.listDTAjax = listDTAjax;
module.exports.listByNamespaceDTAjax = listByNamespaceDTAjax;
module.exports.listWithSegmentByCampaignDTAjax = listWithSegmentByCampaignDTAjax;
module.exports.getByIdTx = getByIdTx;
module.exports.getById = getById;
module.exports.getByIdWithListFields = getByIdWithListFields;
module.exports.getByCidTx = getByCidTx;
module.exports.getByCid = getByCid;
module.exports.create = create;
module.exports.updateWithConsistencyCheck = updateWithConsistencyCheck;
module.exports.remove = remove;