- Fix for #890
- "Channels" feature - Shoutout config param rendered on the homepage - "Clone" feature for campaigns
This commit is contained in:
parent
82251d1cb9
commit
00432e6cfe
23 changed files with 1691 additions and 494 deletions
|
@ -40,6 +40,7 @@ const usersRest = require('./routes/rest/users');
|
|||
const accountRest = require('./routes/rest/account');
|
||||
const reportTemplatesRest = require('./routes/rest/report-templates');
|
||||
const reportsRest = require('./routes/rest/reports');
|
||||
const channelsRest = require('./routes/rest/channels');
|
||||
const campaignsRest = require('./routes/rest/campaigns');
|
||||
const triggersRest = require('./routes/rest/triggers');
|
||||
const listsRest = require('./routes/rest/lists');
|
||||
|
@ -300,6 +301,7 @@ async function createApp(appType) {
|
|||
app.use('/rest', sendConfigurationsRest);
|
||||
app.use('/rest', usersRest);
|
||||
app.use('/rest', accountRest);
|
||||
app.use('/rest', channelsRest);
|
||||
app.use('/rest', campaignsRest);
|
||||
app.use('/rest', triggersRest);
|
||||
app.use('/rest', listsRest);
|
||||
|
|
|
@ -293,49 +293,53 @@ defaultRoles:
|
|||
master:
|
||||
name: Master
|
||||
description: All permissions
|
||||
permissions: [view, edit, delete, share, createNamespace, createList, createCustomForm, createReport, createReportTemplate, createTemplate, createMosaicoTemplate, createSendConfiguration, createCampaign, manageUsers]
|
||||
permissions: [view, edit, delete, share, createNamespace, createList, createCustomForm, createReport, createReportTemplate, createTemplate, createMosaicoTemplate, createSendConfiguration, createChannel, createCampaign, manageUsers]
|
||||
children:
|
||||
sendConfiguration: [viewPublic, viewPrivate, edit, delete, share, sendWithoutOverrides, sendWithAllowedOverrides, sendWithAnyOverrides]
|
||||
list: [view, edit, delete, share, viewFields, manageFields, viewSubscriptions, viewTestSubscriptions, manageSubscriptions, viewSegments, manageSegments, viewImports, manageImports, send, sendToTestUsers]
|
||||
customForm: [view, edit, delete, share]
|
||||
channel: [view, edit, delete, share]
|
||||
campaign: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, send, sendToTestUsers, viewStats, fetchRss]
|
||||
template: [view, edit, delete, share, viewFiles, manageFiles, sendToTestUsers]
|
||||
report: [view, edit, delete, share, execute, viewContent, viewOutput]
|
||||
reportTemplate: [view, edit, delete, share, execute]
|
||||
mosaicoTemplate: [view, edit, delete, share, viewFiles, manageFiles]
|
||||
namespace: [view, edit, delete, share, createNamespace, createList, createCustomForm, createReport, createReportTemplate, createTemplate, createMosaicoTemplate, createSendConfiguration, createCampaign, manageUsers]
|
||||
namespace: [view, edit, delete, share, createNamespace, createList, createCustomForm, createReport, createReportTemplate, createTemplate, createMosaicoTemplate, createSendConfiguration, createChannel, createCampaign, manageUsers]
|
||||
|
||||
campaignsAdmin:
|
||||
name: Campaigns Admin
|
||||
description: In the respective namespace, the user has all permissions for managing lists, templates and campaigns and the permission to send to send configurations.
|
||||
permissions: [view, edit, delete, share, createNamespace, createList, createCustomForm, createReport, createTemplate, createMosaicoTemplate, createCampaign]
|
||||
permissions: [view, edit, delete, share, createNamespace, createList, createCustomForm, createReport, createTemplate, createMosaicoTemplate, createChannel, createCampaign]
|
||||
children:
|
||||
sendConfiguration: [viewPublic, sendWithoutOverrides, sendWithAllowedOverrides]
|
||||
list: [view, edit, delete, share, viewFields, manageFields, viewSubscriptions, viewTestSubscriptions, manageSubscriptions, viewSegments, manageSegments, viewImports, manageImports, send, sendToTestUsers]
|
||||
customForm: [view, edit, delete, share]
|
||||
channel: [view, edit, delete, share]
|
||||
campaign: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, send, sendToTestUsers, viewStats, fetchRss]
|
||||
template: [view, edit, delete, share, viewFiles, manageFiles, sendToTestUsers]
|
||||
report: [view, edit, delete, share, execute, viewContent, viewOutput]
|
||||
reportTemplate: [view, share, execute]
|
||||
mosaicoTemplate: [view, edit, delete, share, viewFiles, manageFiles]
|
||||
namespace: [view, edit, delete, share, createNamespace, createList, createCustomForm, createReport, createTemplate, createMosaicoTemplate, createCampaign]
|
||||
namespace: [view, edit, delete, share, createNamespace, createList, createCustomForm, createReport, createTemplate, createMosaicoTemplate, createChannel, createCampaign]
|
||||
|
||||
campaignsCreator:
|
||||
name: Campaigns Creator
|
||||
description: In the respective namespace, the user has all permissions to create and manage templates and campaigns. The user can also read public data about send configurations and use Mosaico templates in the namespace.
|
||||
permissions: [view, createTemplate, createCampaign]
|
||||
permissions: [view, createTemplate, createChannel, createCampaign]
|
||||
children:
|
||||
sendConfiguration: [viewPublic]
|
||||
channel: [view]
|
||||
campaign: [view, edit, delete, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, sendToTestUsers, viewStats, fetchRss]
|
||||
template: [view, edit, delete, viewFiles, manageFiles, sendToTestUsers]
|
||||
mosaicoTemplate: [view, viewFiles]
|
||||
namespace: [view, createTemplate, createCampaign]
|
||||
namespace: [view, createTemplate, createChannel, createCampaign]
|
||||
|
||||
campaignsViewer:
|
||||
name: Campaigns Viewer
|
||||
description: In the respective namespace, the user has permissions to view campaigns and templates in order to be able to replicate them.
|
||||
permissions: [view, createTemplate, createCampaign]
|
||||
children:
|
||||
channel: [view]
|
||||
campaign: [view, viewFiles, viewAttachments, viewTriggers]
|
||||
template: [view, viewFiles]
|
||||
mosaicoTemplate: [view, viewFiles]
|
||||
|
@ -371,6 +375,16 @@ defaultRoles:
|
|||
description: All permissions
|
||||
permissions: [view, edit, delete, share]
|
||||
|
||||
channel:
|
||||
master:
|
||||
name: Master
|
||||
description: All permissions
|
||||
permissions: [view, edit, delete, createCampaign, share]
|
||||
viewer:
|
||||
name: Viewer
|
||||
description: The user can view the channel but cannot edit it or delete it.
|
||||
permissions: [view]
|
||||
|
||||
campaign:
|
||||
master:
|
||||
name: Master
|
||||
|
|
|
@ -54,6 +54,12 @@ const entityTypes = {
|
|||
},
|
||||
clientLink: id => `/campaigns/${id}`
|
||||
},
|
||||
channel: {
|
||||
entitiesTable: 'channels',
|
||||
sharesTable: 'shares_channel',
|
||||
permissionsTable: 'permissions_channel',
|
||||
clientLink: id => `/channels/${id}`
|
||||
},
|
||||
template: {
|
||||
entitiesTable: 'templates',
|
||||
sharesTable: 'shares_template',
|
||||
|
|
|
@ -28,7 +28,7 @@ const lists = require('./lists');
|
|||
const {EntityActivityType, CampaignActivityType} = require('../../shared/activity-log');
|
||||
const activityLog = require('../lib/activity-log');
|
||||
|
||||
const allowedKeysCommon = ['name', 'description', 'segment', 'namespace',
|
||||
const allowedKeysCommon = ['name', 'description', 'namespace', 'channel',
|
||||
'send_configuration', 'from_name_override', 'from_email_override', 'reply_to_override', 'subject', 'data', 'click_tracking_disabled', 'open_tracking_disabled', 'unsubscribe_url'];
|
||||
|
||||
const allowedKeysCreate = new Set(['type', 'source', ...allowedKeysCommon]);
|
||||
|
@ -67,7 +67,7 @@ function hash(entity, content) {
|
|||
return hasher.hash(filteredEntity);
|
||||
}
|
||||
|
||||
async function _listDTAjax(context, namespaceId, params) {
|
||||
async function _listDTAjax(context, namespaceId, channelId, params) {
|
||||
return await dtHelpers.ajaxListWithPermissions(
|
||||
context,
|
||||
[{ entityTypeId: 'campaign', requiredOperations: ['view'] }],
|
||||
|
@ -75,22 +75,30 @@ async function _listDTAjax(context, namespaceId, params) {
|
|||
builder => {
|
||||
builder = builder.from('campaigns')
|
||||
.innerJoin('namespaces', 'namespaces.id', 'campaigns.namespace')
|
||||
.leftJoin('channels', 'channels.id', 'campaigns.channel')
|
||||
.whereNull('campaigns.parent');
|
||||
if (namespaceId) {
|
||||
builder = builder.where('namespaces.id', namespaceId);
|
||||
}
|
||||
if (channelId) {
|
||||
builder = builder.where('channels.id', channelId);
|
||||
}
|
||||
return builder;
|
||||
},
|
||||
['campaigns.id', 'campaigns.name', 'campaigns.cid', 'campaigns.description', 'campaigns.type', 'campaigns.status', 'campaigns.scheduled', 'campaigns.source', 'campaigns.created', 'namespaces.name']
|
||||
['campaigns.id', 'campaigns.name', 'campaigns.cid', 'campaigns.description', 'campaigns.type', 'channels.name', 'campaigns.status', 'campaigns.scheduled', 'campaigns.source', 'campaigns.created', 'namespaces.name']
|
||||
);
|
||||
}
|
||||
|
||||
async function listDTAjax(context, params) {
|
||||
return await _listDTAjax(context, undefined, params);
|
||||
return await _listDTAjax(context, undefined, undefined, params);
|
||||
}
|
||||
|
||||
async function listByNamespaceDTAjax(context, namespaceId, params) {
|
||||
return await _listDTAjax(context, namespaceId, params);
|
||||
return await _listDTAjax(context, namespaceId, undefined, params);
|
||||
}
|
||||
|
||||
async function listByChannelDTAjax(context, channelId, params) {
|
||||
return await _listDTAjax(context, undefined, channelId, params);
|
||||
}
|
||||
|
||||
async function listChildrenDTAjax(context, campaignId, params) {
|
||||
|
@ -1102,6 +1110,7 @@ module.exports.Content = Content;
|
|||
module.exports.hash = hash;
|
||||
|
||||
module.exports.listDTAjax = listDTAjax;
|
||||
module.exports.listByChannelDTAjax = listByChannelDTAjax;
|
||||
module.exports.listByNamespaceDTAjax = listByNamespaceDTAjax;
|
||||
module.exports.listChildrenDTAjax = listChildrenDTAjax;
|
||||
module.exports.listWithContentDTAjax = listWithContentDTAjax;
|
||||
|
|
206
server/models/channels.js
Normal file
206
server/models/channels.js
Normal file
|
@ -0,0 +1,206 @@
|
|||
'use strict';
|
||||
|
||||
const knex = require('../lib/knex');
|
||||
const hasher = require('node-object-hash')();
|
||||
const dtHelpers = require('../lib/dt-helpers');
|
||||
const interoperableErrors = require('../../shared/interoperable-errors');
|
||||
const shortid = require('shortid');
|
||||
const { enforce, filterObject } = require('../lib/helpers');
|
||||
const shares = require('./shares');
|
||||
const namespaceHelpers = require('../lib/namespace-helpers');
|
||||
const { allTagLanguages } = require('../../shared/templates');
|
||||
const { CampaignSource, } = require('../../shared/campaigns');
|
||||
const segments = require('./segments');
|
||||
const dependencyHelpers = require('../lib/dependency-helpers');
|
||||
|
||||
const {EntityActivityType, CampaignActivityType} = require('../../shared/activity-log');
|
||||
const activityLog = require('../lib/activity-log');
|
||||
|
||||
const allowedKeys = ['name', 'description', 'namespace', 'cpg_name', 'cpg_description',
|
||||
'send_configuration', 'from_name_override', 'from_email_override', 'reply_to_override', 'subject', 'data', 'click_tracking_disabled', 'open_tracking_disabled', 'unsubscribe_url', 'source'];
|
||||
|
||||
|
||||
function hash(entity) {
|
||||
let filteredEntity;
|
||||
|
||||
filteredEntity = filterObject(entity, allowedKeys);
|
||||
filteredEntity.lists = entity.lists;
|
||||
|
||||
return hasher.hash(filteredEntity);
|
||||
}
|
||||
|
||||
async function listDTAjax(context, params) {
|
||||
return await dtHelpers.ajaxListWithPermissions(
|
||||
context,
|
||||
[{ entityTypeId: 'channel', requiredOperations: ['view'] }],
|
||||
params,
|
||||
builder => {
|
||||
builder = builder.from('channels')
|
||||
.innerJoin('namespaces', 'namespaces.id', 'channels.namespace');
|
||||
return builder;
|
||||
},
|
||||
['channels.id', 'channels.name', 'channels.cid', 'channels.description', 'namespaces.name']
|
||||
);
|
||||
}
|
||||
|
||||
async function _getByTx(tx, key, id, withPermissions = true) {
|
||||
const entity = await tx('channels').where('channels.' + key, id)
|
||||
.leftJoin('channel_lists', 'channels.id', 'channel_lists.channel')
|
||||
.groupBy('channels.id')
|
||||
.select([
|
||||
'channels.id', 'channels.name', 'channels.cid', 'channels.description', 'channels.namespace', 'channels.source',
|
||||
'channels.send_configuration', 'channels.from_name_override', 'channels.from_email_override', 'channels.reply_to_override', 'channels.subject',
|
||||
'channels.data', 'channels.click_tracking_disabled', 'channels.open_tracking_disabled', 'channels.unsubscribe_url',
|
||||
knex.raw(`GROUP_CONCAT(CONCAT_WS(\':\', channel_lists.list, channel_lists.segment) ORDER BY channel_lists.id SEPARATOR \';\') as lists`)
|
||||
])
|
||||
.first();
|
||||
|
||||
if (!entity) {
|
||||
throw new shares.throwPermissionDenied();
|
||||
}
|
||||
|
||||
if (entity.lists) {
|
||||
entity.lists = entity.lists.split(';').map(x => {
|
||||
const entries = x.split(':');
|
||||
const list = Number.parseInt(entries[0]);
|
||||
const segment = entries[1] ? Number.parseInt(entries[1]) : null;
|
||||
return {list, segment};
|
||||
});
|
||||
} else {
|
||||
entity.lists = [];
|
||||
}
|
||||
|
||||
entity.data = JSON.parse(entity.data);
|
||||
|
||||
if (withPermissions) {
|
||||
entity.permissions = await shares.getPermissionsTx(tx, context, 'channel', id);
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
async function getByIdTx(tx, context, id, withPermissions = true) {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'channel', id, 'view');
|
||||
|
||||
return await _getByTx(tx, 'id', id, withPermissions);
|
||||
}
|
||||
|
||||
async function getById(context, id, withPermissions = true) {
|
||||
return await knex.transaction(async tx => {
|
||||
return await getByIdTx(tx, context, id, withPermissions);
|
||||
});
|
||||
}
|
||||
|
||||
async function _validateAndPreprocess(tx, context, entity, isCreate) {
|
||||
await namespaceHelpers.validateEntity(tx, entity);
|
||||
|
||||
if (entity.source !== null) {
|
||||
enforce(Number.isInteger(entity.source));
|
||||
|
||||
if (entity.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'template', entity.data.sourceTemplate, 'view');
|
||||
} else if (entity.source === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'campaign', entity.data.sourceCampaign, 'view');
|
||||
} else if (entity.source === CampaignSource.CUSTOM) {
|
||||
enforce(allTagLanguages.includes(entity.data.sourceCustom.tag_language), `Invalid tag language '${entity.data.sourceCustom.tag_language}'`);
|
||||
} else {
|
||||
enforce(false, 'Unknown channel source');
|
||||
}
|
||||
}
|
||||
|
||||
for (const lstSeg of entity.lists) {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'list', lstSeg.list, 'view');
|
||||
|
||||
if (lstSeg.segment) {
|
||||
// Check that the segment under the list exists
|
||||
await segments.getByIdTx(tx, context, lstSeg.list, lstSeg.segment);
|
||||
}
|
||||
}
|
||||
|
||||
if (entity.send_configuration) {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'sendConfiguration', entity.send_configuration, 'viewPublic');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function _createTx(tx, context, entity, content) {
|
||||
return await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createCampaign');
|
||||
|
||||
await _validateAndPreprocess(tx, context, entity, true, content);
|
||||
|
||||
const filteredEntity = filterObject(entity, allowedKeys);
|
||||
filteredEntity.cid = shortid.generate();
|
||||
filteredEntity.data = JSON.stringify(filteredEntity.data);
|
||||
|
||||
const ids = await tx('channels').insert(filteredEntity);
|
||||
const id = ids[0];
|
||||
|
||||
await tx('channel_lists').insert(entity.lists.map(x => ({channel: id, ...x})));
|
||||
|
||||
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'channel', entityId: id });
|
||||
|
||||
await activityLog.logEntityActivity('channel', EntityActivityType.CREATE, id, {});
|
||||
|
||||
return id;
|
||||
});
|
||||
}
|
||||
|
||||
async function create(context, entity) {
|
||||
return await knex.transaction(async tx => {
|
||||
return await _createTx(tx, context, entity);
|
||||
});
|
||||
}
|
||||
|
||||
async function updateWithConsistencyCheck(context, entity) {
|
||||
await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'channel', entity.id, 'edit');
|
||||
|
||||
const existing = await _getByTx(tx, 'id', entity.id, false);
|
||||
|
||||
const existingHash = hash(existing);
|
||||
if (existingHash !== entity.originalHash) {
|
||||
throw new interoperableErrors.ChangedError();
|
||||
}
|
||||
|
||||
await _validateAndPreprocess(tx, context, entity, false);
|
||||
|
||||
let filteredEntity = filterObject(entity, allowedKeys);
|
||||
await namespaceHelpers.validateMove(context, entity, existing, 'channel', 'createCampaign', 'delete');
|
||||
|
||||
await tx('channel_lists').where('channel', entity.id).del();
|
||||
await tx('channel_lists').insert(entity.lists.map(x => ({channel: entity.id, ...x})));
|
||||
|
||||
filteredEntity.data = JSON.stringify(filteredEntity.data);
|
||||
await tx('channels').where('id', entity.id).update(filteredEntity);
|
||||
|
||||
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'channel', entityId: entity.id });
|
||||
|
||||
await activityLog.logEntityActivity('channel', EntityActivityType.UPDATE, entity.id, {});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function remove(context, id) {
|
||||
await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'channel', id, 'delete');
|
||||
|
||||
await dependencyHelpers.ensureNoDependencies(tx, context, id, [
|
||||
{ entityTypeId: 'campaign', column: 'channel' }
|
||||
]);
|
||||
|
||||
await tx('channels').where('id', id).del();
|
||||
|
||||
await activityLog.logEntityActivity('channel', EntityActivityType.REMOVE, id);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
module.exports.hash = hash;
|
||||
|
||||
module.exports.listDTAjax = listDTAjax;
|
||||
module.exports.getByIdTx = getByIdTx;
|
||||
module.exports.getById = getById;
|
||||
module.exports.create = create;
|
||||
module.exports.updateWithConsistencyCheck = updateWithConsistencyCheck;
|
||||
module.exports.remove = remove;
|
|
@ -735,6 +735,8 @@ async function _removeAndGetTx(tx, context, listId, existing) {
|
|||
if (existing.status === SubscriptionStatus.SUBSCRIBED) {
|
||||
await tx('lists').where('id', listId).decrement('subscribers', 1);
|
||||
}
|
||||
|
||||
return existing;
|
||||
}
|
||||
|
||||
async function remove(context, listId, id) {
|
||||
|
|
|
@ -11,6 +11,10 @@ router.postAsync('/campaigns-table', passport.loggedIn, async (req, res) => {
|
|||
return res.json(await campaigns.listDTAjax(req.context, req.body));
|
||||
});
|
||||
|
||||
router.postAsync('/campaigns-by-channel-table/:channelId', passport.loggedIn, async (req, res) => {
|
||||
return res.json(await campaigns.listByChannelDTAjax(req.context, castToInteger(req.params.channelId), req.body));
|
||||
});
|
||||
|
||||
router.postAsync('/campaigns-with-content-table', passport.loggedIn, async (req, res) => {
|
||||
return res.json(await campaigns.listWithContentDTAjax(req.context, req.body));
|
||||
});
|
||||
|
|
38
server/routes/rest/channels.js
Normal file
38
server/routes/rest/channels.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
'use strict';
|
||||
|
||||
const passport = require('../../lib/passport');
|
||||
const channels = require('../../models/channels');
|
||||
|
||||
const router = require('../../lib/router-async').create();
|
||||
const {castToInteger} = require('../../lib/helpers');
|
||||
|
||||
|
||||
router.postAsync('/channels-table', passport.loggedIn, async (req, res) => {
|
||||
return res.json(await channels.listDTAjax(req.context, req.body));
|
||||
});
|
||||
|
||||
router.getAsync('/channels/:channelId', passport.loggedIn, async (req, res) => {
|
||||
const channel = await channels.getById(req.context, castToInteger(req.params.channelId), true);
|
||||
channel.hash = channels.hash(channel);
|
||||
return res.json(channel);
|
||||
});
|
||||
|
||||
router.postAsync('/channels', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||
return res.json(await channels.create(req.context, req.body));
|
||||
});
|
||||
|
||||
router.putAsync('/channels/:channelId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||
const entity = req.body;
|
||||
entity.id = castToInteger(req.params.channelId);
|
||||
|
||||
await channels.updateWithConsistencyCheck(req.context);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
router.deleteAsync('/channels/:channelId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||
await channels.remove(req.context, castToInteger(req.params.channelId));
|
||||
return res.json();
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
54
server/setup/knex/migrations/20200617172500_add_channels.js
Normal file
54
server/setup/knex/migrations/20200617172500_add_channels.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
exports.up = (knex, Promise) => (async() => {
|
||||
await knex.schema.createTable('channels', table => {
|
||||
table.increments('id').primary();
|
||||
table.string('cid').unique().collate('utf8_general_ci');
|
||||
table.string('name');
|
||||
table.text('description');
|
||||
table.string('cpg_name');
|
||||
table.text('cpg_description');
|
||||
table.string('from_email_override');
|
||||
table.string('from_name_override');
|
||||
table.string('reply_to_override');
|
||||
table.string('subject');
|
||||
table.integer('send_configuration').unsigned().references(`send_configurations.id`);
|
||||
table.integer('source').unsigned().notNullable();
|
||||
table.text('data', 'longtext');
|
||||
table.boolean('click_tracking_disabled').defaultTo(false);
|
||||
table.boolean('open_tracking_disabled').defaultTo(false);
|
||||
table.string('unsubscribe_url');
|
||||
table.timestamp('created').defaultTo(knex.fn.now());
|
||||
table.integer('namespace').unsigned().references('namespaces.id');
|
||||
|
||||
});
|
||||
|
||||
await knex.schema.createTable('channel_lists', table => {
|
||||
table.increments('id').primary();
|
||||
table.integer('channel').unsigned().notNullable().references('channels.id');
|
||||
table.integer('list').unsigned().notNullable().references('lists.id');
|
||||
table.integer('segment').unsigned().references('segments.id');
|
||||
});
|
||||
|
||||
await knex.schema.table('campaigns', table => {
|
||||
table.integer('channel').unsigned().references('channels.id');
|
||||
});
|
||||
|
||||
const entityType = 'channel';
|
||||
await knex.schema
|
||||
.createTable(`shares_${entityType}`, table => {
|
||||
table.integer('entity').unsigned().notNullable().references(`${entityType}s.id`).onDelete('CASCADE');
|
||||
table.integer('user').unsigned().notNullable().references('users.id').onDelete('CASCADE');
|
||||
table.string('role', 128).notNullable();
|
||||
table.boolean('auto').defaultTo(false);
|
||||
table.primary(['entity', 'user']);
|
||||
})
|
||||
.createTable(`permissions_${entityType}`, table => {
|
||||
table.integer('entity').unsigned().notNullable().references(`${entityType}s.id`).onDelete('CASCADE');
|
||||
table.integer('user').unsigned().notNullable().references('users.id').onDelete('CASCADE');
|
||||
table.string('operation', 128).notNullable();
|
||||
table.primary(['entity', 'user', 'operation']);
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
exports.down = (knex, Promise) => (async() => {
|
||||
})();
|
Loading…
Add table
Add a link
Reference in a new issue