2017-07-13 11:27:03 +00:00
'use strict' ;
const knex = require ( '../lib/knex' ) ;
2017-08-20 21:44:33 +00:00
const hasher = require ( 'node-object-hash' ) ( ) ;
const shortid = require ( 'shortid' ) ;
2017-07-13 11:27:03 +00:00
const dtHelpers = require ( '../lib/dt-helpers' ) ;
const interoperableErrors = require ( '../shared/interoperable-errors' ) ;
2017-08-13 18:11:58 +00:00
const shares = require ( './shares' ) ;
const fields = require ( './fields' ) ;
2018-09-01 19:29:10 +00:00
const { SubscriptionStatus , getFieldColumn } = require ( '../shared/lists' ) ;
2017-08-19 13:12:22 +00:00
const segments = require ( './segments' ) ;
2017-08-20 21:44:33 +00:00
const { enforce , filterObject } = require ( '../lib/helpers' ) ;
const moment = require ( 'moment' ) ;
2017-08-22 06:15:13 +00:00
const { formatDate , formatBirthday } = require ( '../shared/date' ) ;
2018-09-01 19:29:10 +00:00
const crypto = require ( 'crypto' ) ;
2018-09-09 22:55:44 +00:00
const campaigns = require ( './campaigns' ) ;
2017-08-19 13:12:22 +00:00
2017-08-20 21:44:33 +00:00
const allowedKeysBase = new Set ( [ 'email' , 'tz' , 'is_test' , 'status' ] ) ;
2017-07-13 11:27:03 +00:00
2017-08-22 06:15:13 +00:00
const fieldTypes = { } ;
const Cardinality = {
SINGLE : 0 ,
MULTIPLE : 1
} ;
function getOptionsMap ( groupedField ) {
const result = { } ;
for ( const opt of groupedField . settings . options ) {
result [ opt . key ] = opt . label ;
}
return result ;
}
2018-01-28 22:59:05 +00:00
fieldTypes . text = fieldTypes . website = fieldTypes . longtext = fieldTypes . gpg = fieldTypes . number = fieldTypes . json = {
2017-08-22 06:15:13 +00:00
afterJSON : ( groupedField , entity ) => { } ,
listRender : ( groupedField , value ) => value
} ;
fieldTypes [ 'checkbox-grouped' ] = {
afterJSON : ( groupedField , entity ) => { } ,
listRender : ( groupedField , value ) => {
const optMap = getOptionsMap ( groupedField ) ;
return value . map ( x => optMap [ x ] ) . join ( ', ' ) ;
}
} ;
fieldTypes [ 'radio-enum' ] = fieldTypes [ 'dropdown-enum' ] = fieldTypes [ 'radio-grouped' ] = fieldTypes [ 'dropdown-grouped' ] = {
afterJSON : ( groupedField , entity ) => { } ,
listRender : ( groupedField , value ) => {
const optMap = getOptionsMap ( groupedField ) ;
return optMap [ value ] ;
}
} ;
fieldTypes . date = {
afterJSON : ( groupedField , entity ) => {
2018-09-01 19:29:10 +00:00
const key = getFieldColumn ( groupedField ) ;
2018-01-27 15:37:14 +00:00
if ( key in entity ) {
entity [ key ] = entity [ key ] ? moment ( entity [ key ] ) . toDate ( ) : null ;
}
2017-08-22 06:15:13 +00:00
} ,
listRender : ( groupedField , value ) => formatDate ( groupedField . settings . dateFormat , value )
} ;
fieldTypes . birthday = {
afterJSON : ( groupedField , entity ) => {
2018-09-01 19:29:10 +00:00
const key = getFieldColumn ( groupedField ) ;
2018-01-27 15:37:14 +00:00
if ( key in entity ) {
entity [ key ] = entity [ key ] ? moment ( entity [ key ] ) . toDate ( ) : null ;
}
2017-08-22 06:15:13 +00:00
} ,
listRender : ( groupedField , value ) => formatBirthday ( groupedField . settings . dateFormat , value )
} ;
2017-12-30 11:23:16 +00:00
function getSubscriptionTableName ( listId ) {
2017-08-20 21:44:33 +00:00
return ` subscription__ ${ listId } ` ;
}
async function getGroupedFieldsMap ( tx , listId ) {
const groupedFields = await fields . listGroupedTx ( tx , listId ) ;
const result = { } ;
for ( const fld of groupedFields ) {
2018-09-01 19:29:10 +00:00
result [ getFieldColumn ( fld ) ] = fld ;
2017-08-20 21:44:33 +00:00
}
return result ;
}
function groupSubscription ( groupedFieldsMap , entity ) {
2018-09-01 19:29:10 +00:00
for ( const fldCol in groupedFieldsMap ) {
const fld = groupedFieldsMap [ fldCol ] ;
2017-08-20 21:44:33 +00:00
const fieldType = fields . getFieldType ( fld . type ) ;
if ( fieldType . grouped ) {
let value = null ;
if ( fieldType . cardinality === fields . Cardinality . SINGLE ) {
for ( const optionKey in fld . groupedOptions ) {
const option = fld . groupedOptions [ optionKey ] ;
if ( entity [ option . column ] ) {
value = option . column ;
}
delete entity [ option . column ] ;
}
} else {
value = [ ] ;
for ( const optionKey in fld . groupedOptions ) {
const option = fld . groupedOptions [ optionKey ] ;
if ( entity [ option . column ] ) {
value . push ( option . column ) ;
}
delete entity [ option . column ] ;
}
}
2018-09-01 19:29:10 +00:00
entity [ fldCol ] = value ;
2017-07-13 11:27:03 +00:00
2017-08-20 21:44:33 +00:00
} else if ( fieldType . enumerated ) {
// This is enum-xxx type. We just make sure that the options we give out match the field settings.
// If the field settings gets changed, there can be discrepancies between the field and the subscription data.
const allowedKeys = new Set ( fld . settings . options . map ( x => x . key ) ) ;
2018-09-01 19:29:10 +00:00
if ( ! allowedKeys . has ( entity [ fldCol ] ) ) {
entity [ fldCol ] = null ;
2017-08-20 21:44:33 +00:00
}
}
}
}
function ungroupSubscription ( groupedFieldsMap , entity ) {
2018-09-01 19:29:10 +00:00
for ( const fldCol in groupedFieldsMap ) {
const fld = groupedFieldsMap [ fldCol ] ;
2017-08-20 21:44:33 +00:00
const fieldType = fields . getFieldType ( fld . type ) ;
if ( fieldType . grouped ) {
if ( fieldType . cardinality === fields . Cardinality . SINGLE ) {
2018-09-01 19:29:10 +00:00
const value = entity [ fldCol ] ;
2017-08-20 21:44:33 +00:00
for ( const optionKey in fld . groupedOptions ) {
const option = fld . groupedOptions [ optionKey ] ;
entity [ option . column ] = option . column === value ;
}
} else {
2018-09-01 19:29:10 +00:00
const values = entity [ fldCol ] || [ ] ; // The default (empty array) is here because create may be called with an entity that has some fields not filled in
2017-08-20 21:44:33 +00:00
for ( const optionKey in fld . groupedOptions ) {
const option = fld . groupedOptions [ optionKey ] ;
entity [ option . column ] = values . includes ( option . column ) ;
}
}
2018-09-01 19:29:10 +00:00
delete entity [ fldCol ] ;
2017-08-20 21:44:33 +00:00
} else if ( fieldType . enumerated ) {
// This is enum-xxx type. We just make sure that the options we give out match the field settings.
// If the field settings gets changed, there can be discrepancies between the field and the subscription data.
const allowedKeys = new Set ( fld . settings . options . map ( x => x . key ) ) ;
2018-09-01 19:29:10 +00:00
if ( ! allowedKeys . has ( entity [ fldCol ] ) ) {
entity [ fldCol ] = null ;
2017-08-20 21:44:33 +00:00
}
}
}
}
2017-08-13 18:11:58 +00:00
2017-08-20 21:44:33 +00:00
function getAllowedKeys ( groupedFieldsMap ) {
return new Set ( [
... allowedKeysBase ,
... Object . keys ( groupedFieldsMap )
] ) ;
}
function hashByAllowedKeys ( allowedKeys , entity ) {
2017-08-13 18:11:58 +00:00
return hasher . hash ( filterObject ( entity , allowedKeys ) ) ;
}
2017-08-20 21:44:33 +00:00
async function hashByList ( listId , entity ) {
return await knex . transaction ( async tx => {
const groupedFieldsMap = await getGroupedFieldsMap ( tx , listId ) ;
const allowedKeys = getAllowedKeys ( groupedFieldsMap ) ;
return hashByAllowedKeys ( allowedKeys , entity ) ;
} ) ;
}
2018-09-18 08:30:13 +00:00
async function _getByTx ( tx , context , listId , key , value , grouped ) {
await shares . enforceEntityPermissionTx ( tx , context , 'list' , listId , 'viewSubscriptions' ) ;
2017-12-30 11:23:16 +00:00
2018-09-18 08:30:13 +00:00
const entity = await tx ( getSubscriptionTableName ( listId ) ) . where ( key , value ) . first ( ) ;
2017-08-20 21:44:33 +00:00
2018-09-18 08:30:13 +00:00
if ( ! entity ) {
throw new interoperableErrors . NotFoundError ( 'Subscription not found in this list' ) ;
}
2017-12-30 11:23:16 +00:00
2018-09-18 08:30:13 +00:00
const groupedFieldsMap = await getGroupedFieldsMap ( tx , listId ) ;
2017-12-30 11:23:16 +00:00
2018-09-18 08:30:13 +00:00
if ( grouped ) {
groupSubscription ( groupedFieldsMap , entity ) ;
}
2017-12-30 11:23:16 +00:00
2018-09-18 08:30:13 +00:00
return entity ;
}
2017-08-20 21:44:33 +00:00
2018-09-18 08:30:13 +00:00
async function _getBy ( context , listId , key , value , grouped ) {
return await knex . transaction ( async tx => {
return _getByTx ( tx , context , listId , key , value , grouped ) ;
2017-08-20 21:44:33 +00:00
} ) ;
}
2017-08-13 18:11:58 +00:00
2017-12-30 11:23:16 +00:00
async function getById ( context , listId , id , grouped = true ) {
return await _getBy ( context , listId , 'id' , id , grouped ) ;
2017-12-10 20:44:35 +00:00
}
2017-12-30 11:23:16 +00:00
async function getByEmail ( context , listId , email , grouped = true ) {
return await _getBy ( context , listId , 'email' , email , grouped ) ;
2017-12-10 20:44:35 +00:00
}
2017-12-30 11:23:16 +00:00
async function getByCid ( context , listId , cid , grouped = true ) {
return await _getBy ( context , listId , 'cid' , cid , grouped ) ;
2017-12-10 20:44:35 +00:00
}
2018-09-18 08:30:13 +00:00
async function getByCidTx ( tx , context , listId , cid , grouped = true ) {
return await _getByTx ( tx , context , listId , 'cid' , cid , grouped ) ;
}
2017-08-19 13:12:22 +00:00
async function listDTAjax ( context , listId , segmentId , params ) {
2017-08-13 18:11:58 +00:00
return await knex . transaction ( async tx => {
await shares . enforceEntityPermissionTx ( tx , context , 'list' , listId , 'viewSubscriptions' ) ;
2017-12-30 11:23:16 +00:00
const listTable = getSubscriptionTableName ( listId ) ;
2017-09-17 14:36:23 +00:00
2017-08-22 06:15:13 +00:00
// All the data transformation below is to reuse ajaxListTx and groupSubscription methods so as to keep the code DRY
// We first construct the columns to contain all which is supposed to be show and extraColumns which contain
// everything else that constitutes the subscription.
// Then in ajaxList's mapFunc, we construct the entity from the fields ajaxList retrieved and pass it to groupSubscription
// to group the fields. Then we copy relevant values form grouped subscription to ajaxList's data which then get
// returned to the client. During the copy, we also render the values.
const groupedFieldsMap = await getGroupedFieldsMap ( tx , listId ) ;
const listFlds = await fields . listByOrderListTx ( tx , listId , [ 'column' , 'id' ] ) ;
2017-09-17 14:36:23 +00:00
const columns = [
listTable + '.id' ,
listTable + '.cid' ,
listTable + '.email' ,
listTable + '.status' ,
listTable + '.created' ,
{ name : 'blacklisted' , raw : 'not isnull(blacklist.email)' }
] ;
2017-08-22 06:15:13 +00:00
const extraColumns = [ ] ;
let listFldIdx = columns . length ;
const idxMap = { } ;
for ( const listFld of listFlds ) {
2018-09-01 19:29:10 +00:00
const fldCol = getFieldColumn ( listFld ) ;
const fld = groupedFieldsMap [ fldCol ] ;
2017-08-22 06:15:13 +00:00
if ( fld . column ) {
2017-09-17 14:36:23 +00:00
columns . push ( listTable + '.' + fld . column ) ;
2017-08-22 06:15:13 +00:00
} else {
columns . push ( {
2018-09-01 19:29:10 +00:00
name : listTable + '.' + fldCol ,
2017-08-22 06:15:13 +00:00
raw : 0
} )
}
2018-09-01 19:29:10 +00:00
idxMap [ fldCol ] = listFldIdx ;
2017-08-22 06:15:13 +00:00
listFldIdx += 1 ;
}
2018-09-01 19:29:10 +00:00
for ( const fldCol in groupedFieldsMap ) {
const fld = groupedFieldsMap [ fldCol ] ;
2017-08-22 06:15:13 +00:00
if ( fld . column ) {
2018-09-01 19:29:10 +00:00
if ( ! ( fldCol in idxMap ) ) {
2017-09-17 14:36:23 +00:00
extraColumns . push ( listTable + '.' + fld . column ) ;
2018-09-01 19:29:10 +00:00
idxMap [ fldCol ] = listFldIdx ;
2017-08-22 06:15:13 +00:00
listFldIdx += 1 ;
}
} else {
for ( const optionColumn in fld . groupedOptions ) {
2017-09-17 14:36:23 +00:00
extraColumns . push ( listTable + '.' + optionColumn ) ;
2017-08-22 06:15:13 +00:00
idxMap [ optionColumn ] = listFldIdx ;
listFldIdx += 1 ;
}
}
}
2017-08-19 13:12:22 +00:00
const addSegmentQuery = segmentId ? await segments . getQueryGeneratorTx ( tx , listId , segmentId ) : ( ) => { } ;
2017-08-13 18:11:58 +00:00
return await dtHelpers . ajaxListTx (
tx ,
params ,
2017-08-19 13:12:22 +00:00
builder => {
2017-09-17 14:36:23 +00:00
const query = builder
. from ( listTable )
. leftOuterJoin ( 'blacklist' , listTable + '.email' , 'blacklist.email' )
;
2017-08-19 13:12:22 +00:00
query . where ( function ( ) {
addSegmentQuery ( this ) ;
} ) ;
2018-09-27 10:34:54 +00:00
2017-08-19 13:12:22 +00:00
return query ;
} ,
2017-08-22 06:15:13 +00:00
columns ,
{
mapFun : data => {
const entity = { } ;
2018-09-01 19:29:10 +00:00
for ( const fldCol in idxMap ) {
2017-08-22 06:15:13 +00:00
// This is a bit of hacking. We rely on the fact that if a field has a column, then the column is the field key.
// Then it has the group id with value 0. groupSubscription will be able to process the fields that have a column
// and it will assign values to the fields that don't have a value (i.e. those that currently have the group id and value 0).
2018-09-01 19:29:10 +00:00
entity [ fldCol ] = data [ idxMap [ fldCol ] ] ;
2017-08-22 06:15:13 +00:00
}
groupSubscription ( groupedFieldsMap , entity ) ;
for ( const listFld of listFlds ) {
2018-09-01 19:29:10 +00:00
const fldCol = getFieldColumn ( listFld ) ;
const fld = groupedFieldsMap [ fldCol ] ;
data [ idxMap [ fldCol ] ] = fieldTypes [ fld . type ] . listRender ( fld , entity [ fldCol ] ) ;
2017-08-22 06:15:13 +00:00
}
} ,
extraColumns
}
2017-08-13 18:11:58 +00:00
) ;
} ) ;
}
2018-01-28 22:59:05 +00:00
async function list ( context , listId , grouped = true , offset , limit ) {
2017-08-13 18:11:58 +00:00
return await knex . transaction ( async tx => {
await shares . enforceEntityPermissionTx ( tx , context , 'list' , listId , 'viewSubscriptions' ) ;
2018-01-28 22:59:05 +00:00
const count = await tx ( getSubscriptionTableName ( listId ) ) . count ( '* as count' ) . first ( ) . count ;
2017-08-20 21:44:33 +00:00
2018-01-28 22:59:05 +00:00
const entitiesQry = tx ( getSubscriptionTableName ( listId ) ) . orderBy ( 'id' , 'asc' ) ;
2017-08-20 21:44:33 +00:00
2018-01-28 22:59:05 +00:00
if ( Number . isInteger ( offset ) ) {
entitiesQry . offset ( offset ) ;
}
if ( Number . isInteger ( limit ) ) {
entitiesQry . limit ( limit ) ;
}
const entities = await entitiesQry ;
if ( grouped ) {
const groupedFieldsMap = await getGroupedFieldsMap ( tx , listId ) ;
for ( const entity of entities ) {
groupSubscription ( groupedFieldsMap , entity ) ;
}
2017-08-20 21:44:33 +00:00
}
2018-01-28 22:59:05 +00:00
return {
subscriptions : entities ,
total : count
} ;
2017-08-20 21:44:33 +00:00
} ) ;
}
async function serverValidate ( context , listId , data ) {
return await knex . transaction ( async tx => {
const result = { } ;
await shares . enforceEntityPermissionTx ( tx , context , 'list' , listId , 'manageSubscriptions' ) ;
if ( data . email ) {
2017-12-30 11:23:16 +00:00
const existingKeyQuery = tx ( getSubscriptionTableName ( listId ) ) . where ( 'email' , data . email ) ;
2017-08-20 21:44:33 +00:00
if ( data . id ) {
existingKeyQuery . whereNot ( 'id' , data . id ) ;
}
const existingKey = await existingKeyQuery . first ( ) ;
result . key = {
exists : ! ! existingKey
} ;
}
return result ;
2017-08-13 18:11:58 +00:00
} ) ;
2017-07-13 11:27:03 +00:00
}
2018-01-27 15:37:14 +00:00
async function _validateAndPreprocess ( tx , listId , groupedFieldsMap , entity , meta , isCreate ) {
2017-08-20 21:44:33 +00:00
enforce ( entity . email , 'Email must be set' ) ;
2018-09-01 19:29:10 +00:00
const existingWithKeyQuery = tx ( getSubscriptionTableName ( listId ) ) . where ( 'hash_email' , hashEmail ( entity . email ) ) ;
2017-08-20 21:44:33 +00:00
if ( ! isCreate ) {
existingWithKeyQuery . whereNot ( 'id' , entity . id ) ;
}
const existingWithKey = await existingWithKeyQuery . first ( ) ;
if ( existingWithKey ) {
2018-05-21 17:41:10 +00:00
if ( meta && ( meta . updateAllowed || ( meta . updateOfUnsubscribedAllowed && existingWithKey . status === SubscriptionStatus . UNSUBSCRIBED ) ) ) {
2018-01-28 22:59:05 +00:00
meta . update = true ;
2018-01-27 15:37:14 +00:00
meta . existing = existingWithKey ;
} else {
throw new interoperableErrors . DuplicitEmailError ( ) ;
}
2018-05-21 17:41:10 +00:00
} else {
2018-09-01 19:29:10 +00:00
// This is here because of the API endpoint, which allows one to submit subscriptions without caring about whether they already exist, what their status is, etc.
// The same for import where we need to subscribed only those (existing and new) that have not been unsubscribed already.
// In the case, the subscription is existing, we should not change the status. If it does not exist, we are fine with changing the status to SUBSCRIBED
if ( meta && meta . subscribeIfNoExisting && ! entity . status ) {
2018-05-21 17:41:10 +00:00
entity . status = SubscriptionStatus . SUBSCRIBED ;
}
2017-08-20 21:44:33 +00:00
}
2018-01-28 22:59:05 +00:00
if ( ( isCreate && ! ( meta && meta . update ) ) || 'status' in entity ) {
2018-07-31 04:34:28 +00:00
enforce ( entity . status >= SubscriptionStatus . MIN && entity . status <= SubscriptionStatus . MAX , 'Invalid status' ) ;
2018-01-28 22:59:05 +00:00
}
2017-08-20 21:44:33 +00:00
for ( const key in groupedFieldsMap ) {
const fld = groupedFieldsMap [ key ] ;
2017-08-22 06:15:13 +00:00
fieldTypes [ fld . type ] . afterJSON ( fld , entity ) ;
2017-08-20 21:44:33 +00:00
}
}
2018-09-01 19:29:10 +00:00
function hashEmail ( email ) {
return crypto . createHash ( 'sha512' ) . update ( email ) . digest ( "base64" ) ;
}
function updateSourcesAndHashEmail ( subscription , source , groupedFieldsMap ) {
2018-08-06 14:54:51 +00:00
if ( 'email' in subscription ) {
2018-09-01 19:29:10 +00:00
subscription . hash _email = hashEmail ( subscription . email ) ;
2018-08-06 14:54:51 +00:00
subscription . source _email = source ;
}
2018-09-01 19:29:10 +00:00
for ( const fldCol in groupedFieldsMap ) {
const fld = groupedFieldsMap [ fldCol ] ;
2018-08-06 14:54:51 +00:00
const fieldType = fields . getFieldType ( fld . type ) ;
if ( fieldType . grouped ) {
for ( const optionKey in fld . groupedOptions ) {
const option = fld . groupedOptions [ optionKey ] ;
if ( option . column in subscription ) {
subscription [ 'source_' + option . column ] = source ;
}
}
} else {
2018-09-01 19:29:10 +00:00
if ( fldCol in subscription ) {
subscription [ 'source_' + fldCol ] = source ;
2018-08-06 14:54:51 +00:00
}
}
}
}
2018-01-27 15:37:14 +00:00
async function _update ( tx , listId , existing , filteredEntity ) {
2018-01-28 22:59:05 +00:00
if ( 'status' in filteredEntity ) {
if ( existing . status !== filteredEntity . status ) {
filteredEntity . status _change = new Date ( ) ;
}
2018-01-27 15:37:14 +00:00
}
await tx ( getSubscriptionTableName ( listId ) ) . where ( 'id' , existing . id ) . update ( filteredEntity ) ;
2018-01-28 22:59:05 +00:00
if ( 'status' in filteredEntity ) {
let countIncrement = 0 ;
if ( existing . status === SubscriptionStatus . SUBSCRIBED && filteredEntity . status !== SubscriptionStatus . SUBSCRIBED ) {
countIncrement = - 1 ;
} else if ( existing . status !== SubscriptionStatus . SUBSCRIBED && filteredEntity . status === SubscriptionStatus . SUBSCRIBED ) {
countIncrement = 1 ;
}
2018-01-27 15:37:14 +00:00
2018-01-28 22:59:05 +00:00
if ( countIncrement ) {
await tx ( 'lists' ) . where ( 'id' , listId ) . increment ( 'subscribers' , countIncrement ) ;
}
2018-01-27 15:37:14 +00:00
}
}
async function _create ( tx , listId , filteredEntity ) {
const ids = await tx ( getSubscriptionTableName ( listId ) ) . insert ( filteredEntity ) ;
const id = ids [ 0 ] ;
if ( filteredEntity . status === SubscriptionStatus . SUBSCRIBED ) {
await tx ( 'lists' ) . where ( 'id' , listId ) . increment ( 'subscribers' , 1 ) ;
}
return id ;
}
2018-01-28 22:59:05 +00:00
/ *
Adds a new subscription . Returns error if a subscription with the same email address is already present and is not unsubscribed .
2018-05-21 17:41:10 +00:00
If it is unsubscribed and meta . updateOfUnsubscribedAllowed , the existing subscription is changed based on the provided data .
If meta . updateAllowed is true , it updates even an active subscription .
2018-01-28 22:59:05 +00:00
* /
2018-09-01 19:29:10 +00:00
async function createTxWithGroupedFieldsMap ( tx , context , listId , groupedFieldsMap , entity , source , meta ) {
await shares . enforceEntityPermissionTx ( tx , context , 'list' , listId , 'manageSubscriptions' ) ;
2017-08-20 21:44:33 +00:00
2018-09-01 19:29:10 +00:00
const allowedKeys = getAllowedKeys ( groupedFieldsMap ) ;
2017-08-20 21:44:33 +00:00
2018-09-01 19:29:10 +00:00
await _validateAndPreprocess ( tx , listId , groupedFieldsMap , entity , meta , true ) ;
2017-08-20 21:44:33 +00:00
2018-09-01 19:29:10 +00:00
const filteredEntity = filterObject ( entity , allowedKeys ) ;
filteredEntity . status _change = new Date ( ) ;
2017-08-20 21:44:33 +00:00
2018-09-01 19:29:10 +00:00
ungroupSubscription ( groupedFieldsMap , filteredEntity ) ;
2018-08-06 14:54:51 +00:00
2018-09-01 19:29:10 +00:00
updateSourcesAndHashEmail ( filteredEntity , source , groupedFieldsMap ) ;
2017-08-20 21:44:33 +00:00
2018-09-01 19:29:10 +00:00
filteredEntity . opt _in _ip = meta && meta . ip ;
filteredEntity . opt _in _country = meta && meta . country ;
2017-08-20 21:44:33 +00:00
2018-09-01 19:29:10 +00:00
if ( meta && meta . update ) { // meta.update is set by _validateAndPreprocess
await _update ( tx , listId , meta . existing , filteredEntity ) ;
meta . cid = meta . existing . cid ; // The cid is needed by /confirm/subscribe/:cid
return meta . existing . id ;
} else {
filteredEntity . cid = shortid . generate ( ) ;
2017-08-20 21:44:33 +00:00
2018-09-01 19:29:10 +00:00
if ( meta ) {
meta . cid = filteredEntity . cid ; // The cid is needed by /confirm/subscribe/:cid
2018-01-27 15:37:14 +00:00
}
2018-09-01 19:29:10 +00:00
return await _create ( tx , listId , filteredEntity ) ;
}
}
async function create ( context , listId , entity , source , meta ) {
return await knex . transaction ( async tx => {
const groupedFieldsMap = await getGroupedFieldsMap ( tx , listId ) ;
return await createTxWithGroupedFieldsMap ( tx , context , listId , groupedFieldsMap , entity , source , meta ) ;
2017-08-20 21:44:33 +00:00
} ) ;
}
2018-08-06 14:54:51 +00:00
async function updateWithConsistencyCheck ( context , listId , entity , source ) {
2017-08-20 21:44:33 +00:00
await knex . transaction ( async tx => {
await shares . enforceEntityPermissionTx ( tx , context , 'list' , listId , 'manageSubscriptions' ) ;
2017-12-30 11:23:16 +00:00
const existing = await tx ( getSubscriptionTableName ( listId ) ) . where ( 'id' , entity . id ) . first ( ) ;
2017-08-20 21:44:33 +00:00
if ( ! existing ) {
throw new interoperableErrors . NotFoundError ( ) ;
}
const groupedFieldsMap = await getGroupedFieldsMap ( tx , listId ) ;
const allowedKeys = getAllowedKeys ( groupedFieldsMap ) ;
groupSubscription ( groupedFieldsMap , existing ) ;
const existingHash = hashByAllowedKeys ( allowedKeys , existing ) ;
if ( existingHash !== entity . originalHash ) {
throw new interoperableErrors . ChangedError ( ) ;
}
2018-01-27 15:37:14 +00:00
await _validateAndPreprocess ( tx , listId , groupedFieldsMap , entity , null , false ) ;
2017-08-20 21:44:33 +00:00
const filteredEntity = filterObject ( entity , allowedKeys ) ;
ungroupSubscription ( groupedFieldsMap , filteredEntity ) ;
2018-09-01 19:29:10 +00:00
updateSourcesAndHashEmail ( filteredEntity , source , groupedFieldsMap ) ;
2018-08-06 14:54:51 +00:00
2018-01-27 15:37:14 +00:00
await _update ( tx , listId , existing , filteredEntity ) ;
2017-08-20 21:44:33 +00:00
} ) ;
}
2018-01-28 22:59:05 +00:00
async function _removeAndGetTx ( tx , context , listId , existing ) {
2017-08-20 21:44:33 +00:00
await shares . enforceEntityPermissionTx ( tx , context , 'list' , listId , 'manageSubscriptions' ) ;
if ( ! existing ) {
throw new interoperableErrors . NotFoundError ( ) ;
}
2017-12-30 11:23:16 +00:00
await tx ( getSubscriptionTableName ( listId ) ) . where ( 'id' , id ) . del ( ) ;
2017-08-20 21:44:33 +00:00
if ( existing . status === SubscriptionStatus . SUBSCRIBED ) {
await tx ( 'lists' ) . where ( 'id' , listId ) . decrement ( 'subscribers' , 1 ) ;
}
}
async function remove ( context , listId , id ) {
await knex . transaction ( async tx => {
2018-01-28 22:59:05 +00:00
const existing = await tx ( getSubscriptionTableName ( listId ) ) . where ( 'id' , id ) . first ( ) ;
await _removeAndGetTx ( tx , context , listId , existing ) ;
2017-08-20 21:44:33 +00:00
} ) ;
}
2018-01-28 22:59:05 +00:00
async function removeByEmailAndGet ( context , listId , email ) {
return await knex . transaction ( async tx => {
2018-09-01 19:29:10 +00:00
const existing = await tx ( getSubscriptionTableName ( listId ) ) . where ( 'hash_email' , hashEmail ( email ) ) . first ( ) ;
2018-01-28 22:59:05 +00:00
return await _removeAndGetTx ( tx , context , listId , existing ) ;
} ) ;
}
2018-09-09 22:55:44 +00:00
async function _changeStatusTx ( tx , context , listId , existing , newStatus ) {
enforce ( newStatus !== SubscriptionStatus . SUBSCRIBED ) ;
2017-08-20 21:44:33 +00:00
2018-09-09 22:55:44 +00:00
await shares . enforceEntityPermissionTx ( tx , context , 'list' , listId , 'manageSubscriptions' ) ;
2017-12-10 20:44:35 +00:00
2018-09-09 22:55:44 +00:00
await tx ( getSubscriptionTableName ( listId ) ) . where ( 'id' , existing . id ) . update ( {
status : newStatus
2018-01-27 15:37:14 +00:00
} ) ;
2017-12-30 11:23:16 +00:00
2018-09-09 22:55:44 +00:00
if ( existing . status === SubscriptionStatus . SUBSCRIBED ) {
await tx ( 'lists' ) . where ( 'id' , listId ) . decrement ( 'subscribers' , 1 ) ;
}
}
2017-08-20 21:44:33 +00:00
2018-09-09 22:55:44 +00:00
async function _unsubscribeExistingAndGetTx ( tx , context , listId , existing ) {
if ( ! ( existing && existing . status === SubscriptionStatus . SUBSCRIBED ) ) {
throw new interoperableErrors . NotFoundError ( ) ;
}
2017-12-30 11:23:16 +00:00
2018-09-09 22:55:44 +00:00
await _changeStatusTx ( tx , context , listId , existing , SubscriptionStatus . UNSUBSCRIBED ) ;
2017-12-30 11:23:16 +00:00
2018-09-09 22:55:44 +00:00
existing . status = SubscriptionStatus . SUBSCRIBED ;
2017-12-10 20:44:35 +00:00
2018-09-09 22:55:44 +00:00
return existing ;
2018-01-27 15:37:14 +00:00
}
async function unsubscribeByIdAndGet ( context , listId , subscriptionId ) {
return await knex . transaction ( async tx => {
const existing = await tx ( getSubscriptionTableName ( listId ) ) . where ( 'id' , subscriptionId ) . first ( ) ;
2018-09-09 22:55:44 +00:00
return await _unsubscribeExistingAndGetTx ( tx , context , listId , existing ) ;
2018-01-27 15:37:14 +00:00
} ) ;
}
async function unsubscribeByCidAndGet ( context , listId , subscriptionCid , campaignCid ) {
return await knex . transaction ( async tx => {
const existing = await tx ( getSubscriptionTableName ( listId ) ) . where ( 'cid' , subscriptionCid ) . first ( ) ;
2018-09-09 22:55:44 +00:00
if ( campaignCid ) {
await campaigns . changeStatusByCampaignCidAndSubscriptionIdTx ( tx , context , campaignCid , listId , existing . id , SubscriptionStatus . UNSUBSCRIBED ) ;
}
return await _unsubscribeExistingAndGetTx ( tx , context , listId , existing ) ;
2017-08-20 21:44:33 +00:00
} ) ;
}
2018-09-01 19:29:10 +00:00
async function unsubscribeByEmailAndGetTx ( tx , context , listId , email ) {
const existing = await tx ( getSubscriptionTableName ( listId ) ) . where ( 'hash_email' , hashEmail ( email ) ) . first ( ) ;
2018-09-09 22:55:44 +00:00
return await _unsubscribeExistingAndGetTx ( tx , context , listId , existing ) ;
2018-09-01 19:29:10 +00:00
}
2018-01-28 22:59:05 +00:00
async function unsubscribeByEmailAndGet ( context , listId , email ) {
return await knex . transaction ( async tx => {
2018-09-01 19:29:10 +00:00
return await unsubscribeByEmailAndGetTx ( tx , context , listId , email ) ;
2018-01-28 22:59:05 +00:00
} ) ;
}
2018-09-09 22:55:44 +00:00
async function changeStatusTx ( tx , context , listId , subscriptionId , subscriptionStatus ) {
const existing = await tx ( getSubscriptionTableName ( listId ) ) . where ( 'id' , subscriptionId ) . first ( ) ;
await _changeStatusTx ( tx , context , listId , existing , subscriptionStatus ) ;
}
2017-12-10 20:44:35 +00:00
async function updateAddressAndGet ( context , listId , subscriptionId , emailNew ) {
return await knex . transaction ( async tx => {
await shares . enforceEntityPermissionTx ( tx , context , 'list' , listId , 'manageSubscriptions' ) ;
2017-08-20 21:44:33 +00:00
2017-12-30 11:23:16 +00:00
const existing = await tx ( getSubscriptionTableName ( listId ) ) . where ( 'id' , subscriptionId ) . first ( ) ;
2017-12-10 20:44:35 +00:00
if ( ! existing ) {
throw new interoperableErrors . NotFoundError ( ) ;
}
2018-01-28 22:59:05 +00:00
if ( existing . email !== emailNew ) {
2018-09-01 19:29:10 +00:00
await tx ( getSubscriptionTableName ( listId ) ) . where ( 'hash_email' , hashEmail ( emailNew ) ) . del ( ) ;
2018-01-28 22:59:05 +00:00
await tx ( getSubscriptionTableName ( listId ) ) . where ( 'id' , subscriptionId ) . update ( {
email : emailNew
} ) ;
existing . email = emailNew ;
}
2017-12-10 20:44:35 +00:00
return existing ;
} ) ;
}
2017-07-13 11:27:03 +00:00
2018-01-28 22:59:05 +00:00
async function updateManaged ( context , listId , cid , entity ) {
2017-12-30 11:23:16 +00:00
await knex . transaction ( async tx => {
await shares . enforceEntityPermissionTx ( tx , context , 'list' , listId , 'manageSubscriptions' ) ;
2018-01-28 22:59:05 +00:00
const groupedFieldsMap = await getGroupedFieldsMap ( tx , listId ) ;
2017-12-30 11:23:16 +00:00
const update = { } ;
2018-01-28 22:59:05 +00:00
for ( const key in groupedFieldsMap ) {
const fld = groupedFieldsMap [ key ] ;
2017-12-30 11:23:16 +00:00
if ( fld . order _manage ) {
2018-01-28 22:59:05 +00:00
update [ key ] = entity [ key ] ;
}
fieldTypes [ fld . type ] . afterJSON ( fld , update ) ;
}
2017-12-30 11:23:16 +00:00
2018-01-28 22:59:05 +00:00
ungroupSubscription ( groupedFieldsMap , update ) ;
await tx ( getSubscriptionTableName ( listId ) ) . where ( 'cid' , cid ) . update ( update ) ;
} ) ;
}
async function getListsWithEmail ( context , email ) {
// FIXME - this methods is rather suboptimal if there are many lists. It quite needs permission caching in shares.js
return await knex . transaction ( async tx => {
const lists = await tx ( 'lists' ) . select ( [ 'id' , 'name' ] ) ;
const result = [ ] ;
for ( const list of lists ) {
await shares . enforceEntityPermissionTx ( tx , context , 'list' , list . id , 'viewSubscriptions' ) ;
const entity = await tx ( getSubscriptionTableName ( list . id ) ) . where ( 'email' , email ) . first ( ) ;
if ( entity ) {
result . push ( list ) ;
2017-12-30 11:23:16 +00:00
}
}
2018-01-28 22:59:05 +00:00
return result ;
2017-12-30 11:23:16 +00:00
} ) ;
}
2018-09-09 22:55:44 +00:00
module . exports . getSubscriptionTableName = getSubscriptionTableName ;
module . exports . hashByList = hashByList ;
module . exports . getById = getById ;
2018-09-18 08:30:13 +00:00
module . exports . getByCidTx = getByCidTx ;
2018-09-09 22:55:44 +00:00
module . exports . getByCid = getByCid ;
module . exports . getByEmail = getByEmail ;
module . exports . list = list ;
module . exports . listDTAjax = listDTAjax ;
module . exports . serverValidate = serverValidate ;
module . exports . create = create ;
module . exports . getGroupedFieldsMap = getGroupedFieldsMap ;
module . exports . createTxWithGroupedFieldsMap = createTxWithGroupedFieldsMap ;
module . exports . updateWithConsistencyCheck = updateWithConsistencyCheck ;
module . exports . remove = remove ;
module . exports . removeByEmailAndGet = removeByEmailAndGet ;
module . exports . unsubscribeByCidAndGet = unsubscribeByCidAndGet ;
module . exports . unsubscribeByIdAndGet = unsubscribeByIdAndGet ;
module . exports . unsubscribeByEmailAndGet = unsubscribeByEmailAndGet ;
module . exports . unsubscribeByEmailAndGetTx = unsubscribeByEmailAndGetTx ;
module . exports . updateAddressAndGet = updateAddressAndGet ;
module . exports . updateManaged = updateManaged ;
module . exports . getListsWithEmail = getListsWithEmail ;
module . exports . changeStatusTx = changeStatusTx ;