2017-07-13 11:27:03 +00:00
'use strict' ;
const knex = require ( '../lib/knex' ) ;
2017-08-11 06:51:30 +00:00
const hasher = require ( 'node-object-hash' ) ( ) ;
const slugify = require ( 'slugify' ) ;
const { enforce , filterObject } = require ( '../lib/helpers' ) ;
2017-07-13 11:27:03 +00:00
const dtHelpers = require ( '../lib/dt-helpers' ) ;
const interoperableErrors = require ( '../shared/interoperable-errors' ) ;
2017-08-11 06:51:30 +00:00
const shares = require ( './shares' ) ;
const validators = require ( '../shared/validators' ) ;
2017-08-11 16:16:44 +00:00
const shortid = require ( 'shortid' ) ;
2017-08-14 20:53:29 +00:00
const segments = require ( './segments' ) ;
2018-01-28 22:59:05 +00:00
const { formatDate , formatBirthday , parseDate , parseBirthday } = require ( '../shared/date' ) ;
2018-09-01 19:29:10 +00:00
const { getFieldColumn } = require ( '../shared/lists' ) ;
2018-01-28 22:59:05 +00:00
const { cleanupFromPost } = require ( '../lib/helpers' ) ;
2017-08-11 06:51:30 +00:00
const allowedKeysCreate = new Set ( [ 'name' , 'key' , 'default_value' , 'type' , 'group' , 'settings' ] ) ;
const allowedKeysUpdate = new Set ( [ 'name' , 'key' , 'default_value' , 'group' , 'settings' ] ) ;
const hashKeys = allowedKeysCreate ;
const fieldTypes = { } ;
2017-08-20 21:44:33 +00:00
const Cardinality = {
SINGLE : 0 ,
MULTIPLE : 1
} ;
2018-01-28 22:59:05 +00:00
fieldTypes . text = {
validate : field => { } ,
2017-08-11 06:51:30 +00:00
addColumn : ( table , name ) => table . string ( name ) ,
indexed : true ,
2017-08-20 21:44:33 +00:00
grouped : false ,
enumerated : false ,
2018-01-28 22:59:05 +00:00
cardinality : Cardinality . SINGLE ,
getHbsType : field => 'typeText' ,
forHbs : ( field , value ) => value ,
parsePostValue : ( field , value ) => value
} ;
fieldTypes . website = {
validate : field => { } ,
addColumn : ( table , name ) => table . string ( name ) ,
indexed : true ,
grouped : false ,
enumerated : false ,
cardinality : Cardinality . SINGLE ,
getHbsType : field => 'typeWebsite' ,
forHbs : ( field , value ) => value ,
parsePostValue : ( field , value ) => value
2017-08-11 06:51:30 +00:00
} ;
2018-01-28 22:59:05 +00:00
fieldTypes . longtext = {
validate : field => { } ,
2017-08-11 06:51:30 +00:00
addColumn : ( table , name ) => table . text ( name ) ,
indexed : false ,
2017-08-20 21:44:33 +00:00
grouped : false ,
enumerated : false ,
2018-01-28 22:59:05 +00:00
cardinality : Cardinality . SINGLE ,
getHbsType : field => 'typeLongtext' ,
forHbs : ( field , value ) => value ,
parsePostValue : ( field , value ) => value
} ;
fieldTypes . gpg = {
validate : field => { } ,
addColumn : ( table , name ) => table . text ( name ) ,
indexed : false ,
grouped : false ,
enumerated : false ,
cardinality : Cardinality . SINGLE ,
getHbsType : field => 'typeGpg' ,
forHbs : ( field , value ) => value ,
parsePostValue : ( field , value ) => value
2017-08-11 06:51:30 +00:00
} ;
fieldTypes . json = {
2018-01-28 22:59:05 +00:00
validate : field => { } ,
2017-08-11 06:51:30 +00:00
addColumn : ( table , name ) => table . json ( name ) ,
indexed : false ,
2017-08-20 21:44:33 +00:00
grouped : false ,
enumerated : false ,
2018-01-28 22:59:05 +00:00
cardinality : Cardinality . SINGLE ,
getHbsType : field => 'typeJson' ,
forHbs : ( field , value ) => value ,
parsePostValue : ( field , value ) => value
2017-08-11 06:51:30 +00:00
} ;
fieldTypes . number = {
2018-01-28 22:59:05 +00:00
validate : field => { } ,
2017-08-11 06:51:30 +00:00
addColumn : ( table , name ) => table . integer ( name ) ,
indexed : true ,
2017-08-20 21:44:33 +00:00
grouped : false ,
enumerated : false ,
2018-01-28 22:59:05 +00:00
cardinality : Cardinality . SINGLE ,
getHbsType : field => 'typeNumber' ,
forHbs : ( field , value ) => value ,
parsePostValue : ( field , value ) => Number ( value )
2017-08-11 06:51:30 +00:00
} ;
2017-08-20 21:44:33 +00:00
fieldTypes [ 'checkbox-grouped' ] = {
2018-01-28 22:59:05 +00:00
validate : field => { } ,
2017-08-11 06:51:30 +00:00
indexed : true ,
2017-08-20 21:44:33 +00:00
grouped : true ,
enumerated : false ,
2018-01-28 22:59:05 +00:00
cardinality : Cardinality . MULTIPLE ,
getHbsType : field => 'typeCheckboxGrouped'
2017-08-20 21:44:33 +00:00
} ;
2018-01-28 22:59:05 +00:00
fieldTypes [ 'radio-grouped' ] = {
validate : field => { } ,
2017-08-20 21:44:33 +00:00
indexed : true ,
grouped : true ,
enumerated : false ,
2018-01-28 22:59:05 +00:00
cardinality : Cardinality . SINGLE ,
getHbsType : field => 'typeRadioGrouped'
} ;
fieldTypes [ 'dropdown-grouped' ] = {
validate : field => { } ,
indexed : true ,
grouped : true ,
enumerated : false ,
cardinality : Cardinality . SINGLE ,
getHbsType : field => 'typeDropdownGrouped'
2017-08-11 06:51:30 +00:00
} ;
2018-01-28 22:59:05 +00:00
fieldTypes [ 'radio-enum' ] = {
validate : field => {
enforce ( field . settings . options , 'Options missing in settings' ) ;
enforce ( field . default _value === null || field . settings . options . find ( x => x . key === field . default _value ) , 'Default value not present in options' ) ;
2017-08-11 06:51:30 +00:00
} ,
addColumn : ( table , name ) => table . string ( name ) ,
indexed : true ,
2017-08-20 21:44:33 +00:00
grouped : false ,
enumerated : true ,
2018-01-28 22:59:05 +00:00
cardinality : Cardinality . SINGLE ,
getHbsType : field => 'typeRadioEnum'
} ;
fieldTypes [ 'dropdown-enum' ] = {
validate : field => {
enforce ( field . settings . options , 'Options missing in settings' ) ;
enforce ( field . default _value === null || field . settings . options . find ( x => x . key === field . default _value ) , 'Default value not present in options' ) ;
} ,
addColumn : ( table , name ) => table . string ( name ) ,
indexed : true ,
grouped : false ,
enumerated : true ,
cardinality : Cardinality . SINGLE ,
getHbsType : field => 'typeDropdownEnum'
2017-08-11 06:51:30 +00:00
} ;
fieldTypes . option = {
2018-01-28 22:59:05 +00:00
validate : field => { } ,
2017-08-11 06:51:30 +00:00
addColumn : ( table , name ) => table . boolean ( name ) ,
indexed : true ,
2017-08-20 21:44:33 +00:00
grouped : false ,
enumerated : false ,
2018-09-01 19:29:10 +00:00
cardinality : Cardinality . SINGLE ,
parsePostValue : ( field , value ) => ! ( [ 'false' , 'no' , '0' , '' ] . indexOf ( ( value || '' ) . toString ( ) . trim ( ) . toLowerCase ( ) ) >= 0 )
2017-08-11 06:51:30 +00:00
} ;
2018-01-28 22:59:05 +00:00
fieldTypes [ 'date' ] = {
validate : field => {
enforce ( [ 'eur' , 'us' ] . includes ( field . settings . dateFormat ) , 'Date format incorrect' ) ;
2017-08-11 06:51:30 +00:00
} ,
addColumn : ( table , name ) => table . dateTime ( name ) ,
indexed : true ,
2017-08-20 21:44:33 +00:00
grouped : false ,
enumerated : false ,
2018-01-28 22:59:05 +00:00
cardinality : Cardinality . SINGLE ,
getHbsType : field => 'typeDate' + field . settings . dateFormat . charAt ( 0 ) . toUpperCase ( ) + field . settings . dateFormat . slice ( 1 ) ,
forHbs : ( field , value ) => formatDate ( field . settings . dateFormat , value ) ,
parsePostValue : ( field , value ) => parseDate ( field . settings . dateFormat , value )
} ;
fieldTypes [ 'birthday' ] = {
validate : field => {
enforce ( [ 'eur' , 'us' ] . includes ( field . settings . dateFormat ) , 'Date format incorrect' ) ;
} ,
addColumn : ( table , name ) => table . dateTime ( name ) ,
indexed : true ,
grouped : false ,
enumerated : false ,
cardinality : Cardinality . SINGLE ,
getHbsType : field => 'typeBirthday' + field . settings . dateFormat . charAt ( 0 ) . toUpperCase ( ) + field . settings . dateFormat . slice ( 1 ) ,
forHbs : ( field , value ) => formatBirthday ( field . settings . dateFormat , value ) ,
parsePostValue : ( field , value ) => parseBirthday ( field . settings . dateFormat , value )
2017-08-11 06:51:30 +00:00
} ;
2017-08-11 22:41:02 +00:00
const groupedTypes = Object . keys ( fieldTypes ) . filter ( key => fieldTypes [ key ] . grouped ) ;
2017-08-11 06:51:30 +00:00
2017-08-20 21:44:33 +00:00
function getFieldType ( type ) {
return fieldTypes [ type ] ;
}
2017-08-11 06:51:30 +00:00
function hash ( entity ) {
return hasher . hash ( filterObject ( entity , hashKeys ) ) ;
}
async function getById ( context , listId , id ) {
2017-08-13 18:11:58 +00:00
return await knex . transaction ( async tx => {
2018-07-31 04:34:28 +00:00
await shares . enforceEntityPermissionTx ( tx , context , 'list' , listId , 'viewFields' ) ;
2017-08-11 06:51:30 +00:00
2017-08-13 18:11:58 +00:00
const entity = await tx ( 'custom_fields' ) . where ( { list : listId , id } ) . first ( ) ;
2017-08-11 06:51:30 +00:00
2017-08-20 21:44:33 +00:00
entity . settings = JSON . parse ( entity . settings ) ;
2017-08-11 06:51:30 +00:00
const orderFields = {
order _list : 'orderListBefore' ,
order _subscribe : 'orderSubscribeBefore' ,
order _manage : 'orderManageBefore'
} ;
for ( const key in orderFields ) {
if ( entity [ key ] !== null ) {
const orderIdRow = await tx ( 'custom_fields' ) . where ( 'list' , listId ) . where ( key , '>' , entity [ key ] ) . orderBy ( key , 'asc' ) . select ( [ 'id' ] ) . first ( ) ;
if ( orderIdRow ) {
entity [ orderFields [ key ] ] = orderIdRow . id ;
} else {
entity [ orderFields [ key ] ] = 'end' ;
}
} else {
entity [ orderFields [ key ] ] = 'none' ;
}
}
2017-08-13 18:11:58 +00:00
return entity ;
} ) ;
2017-08-11 06:51:30 +00:00
}
2017-08-19 13:12:22 +00:00
async function listTx ( tx , listId ) {
2017-08-22 06:15:13 +00:00
return await tx ( 'custom_fields' ) . where ( { list : listId } ) . select ( [ 'id' , 'name' , 'type' , 'key' , 'column' , 'settings' , 'group' , 'default_value' , 'order_list' , 'order_subscribe' , 'order_manage' ] ) . orderBy ( knex . raw ( '-order_list' ) , 'desc' ) . orderBy ( 'id' , 'asc' ) ;
2017-08-19 13:12:22 +00:00
}
2017-08-11 06:51:30 +00:00
async function list ( context , listId ) {
2017-08-13 18:11:58 +00:00
return await knex . transaction ( async tx => {
2018-07-31 04:34:28 +00:00
await shares . enforceEntityPermissionTx ( tx , context , 'list' , listId , [ 'viewFields' ] ) ;
2017-08-19 13:12:22 +00:00
return await listTx ( tx , listId ) ;
2017-08-11 06:51:30 +00:00
} ) ;
2017-08-13 18:11:58 +00:00
}
2017-08-11 06:51:30 +00:00
2017-08-20 21:44:33 +00:00
async function listGroupedTx ( tx , listId ) {
2017-08-22 06:15:13 +00:00
const flds = await listTx ( tx , listId ) ;
2017-08-20 21:44:33 +00:00
const fldsById = { } ;
for ( const fld of flds ) {
fld . settings = JSON . parse ( fld . settings ) ;
fldsById [ fld . id ] = fld ;
if ( fieldTypes [ fld . type ] . grouped ) {
fld . settings . options = [ ] ;
fld . groupedOptions = { } ;
}
}
for ( const fld of flds ) {
if ( fld . group ) {
const group = fldsById [ fld . group ] ;
group . settings . options . push ( { key : fld . column , label : fld . name } ) ;
group . groupedOptions [ fld . column ] = fld ;
}
}
const groupedFlds = flds . filter ( fld => ! fld . group ) ;
for ( const fld of flds ) {
delete fld . group ;
}
return groupedFlds ;
}
async function listGrouped ( context , listId ) {
return await knex . transaction ( async tx => {
2017-12-10 20:44:35 +00:00
// It may seem odd why there is not 'manageFields' here. But it's just a result of strictly apply the "need-to-know" principle. Simply, at this point this function is needed only in managing subscriptions.
2017-08-20 21:44:33 +00:00
await shares . enforceEntityPermissionTx ( tx , context , 'list' , listId , [ 'manageSubscriptions' ] ) ;
return await listGroupedTx ( tx , listId ) ;
} ) ;
}
2017-08-13 18:11:58 +00:00
async function listByOrderListTx ( tx , listId , extraColumns = [ ] ) {
2017-08-22 06:15:13 +00:00
return await tx ( 'custom_fields' ) . where ( { list : listId } ) . whereNotNull ( 'order_list' ) . select ( [ 'name' , 'type' , ... extraColumns ] ) . orderBy ( 'order_list' , 'asc' ) ;
2017-08-11 06:51:30 +00:00
}
async function listDTAjax ( context , listId , params ) {
2017-08-19 13:12:22 +00:00
return await knex . transaction ( async tx => {
2018-07-31 04:34:28 +00:00
await shares . enforceEntityPermissionTx ( tx , context , 'list' , listId , 'viewFields' ) ;
2017-08-19 13:12:22 +00:00
return await dtHelpers . ajaxListTx (
tx ,
params ,
builder => builder
. from ( 'custom_fields' )
// This self join is to provide 'option' fields a reference to their parent grouped field. If the field is not an option, it refers to itself
// All this is to show options always below their group parent
. innerJoin ( 'custom_fields AS parent_fields' , function ( ) {
this . on ( function ( ) {
this . on ( 'custom_fields.type' , '=' , knex . raw ( '?' , [ 'option' ] ) )
. on ( 'custom_fields.group' , '=' , 'parent_fields.id' ) ;
} ) . orOn ( function ( ) {
this . on ( 'custom_fields.type' , '<>' , knex . raw ( '?' , [ 'option' ] ) )
. on ( 'custom_fields.id' , '=' , 'parent_fields.id' ) ;
} ) ;
} )
. where ( 'custom_fields.list' , listId ) ,
[ 'custom_fields.id' , 'custom_fields.name' , 'custom_fields.type' , 'custom_fields.key' , 'custom_fields.order_list' ] ,
{
orderByBuilder : ( builder , orderColumn , orderDir ) => {
// We use here parent_fields to keep options always below their parent group
if ( orderColumn === 'custom_fields.order_list' ) {
builder
. orderBy ( knex . raw ( '-parent_fields.order_list' ) , orderDir === 'asc' ? 'desc' : 'asc' ) // This is MySQL speciality. It sorts the rows in ascending order with NULL values coming last
. orderBy ( 'parent_fields.name' , orderDir )
. orderBy ( knex . raw ( 'custom_fields.type = "option"' ) , 'asc' )
} else {
const parentColumn = orderColumn . replace ( /^custom_fields/ , 'parent_fields' ) ;
builder
. orderBy ( parentColumn , orderDir )
. orderBy ( 'parent_fields.name' , orderDir )
. orderBy ( knex . raw ( 'custom_fields.type = "option"' ) , 'asc' ) ;
}
2017-08-11 06:51:30 +00:00
}
}
2017-08-19 13:12:22 +00:00
) ;
} ) ;
2017-08-11 06:51:30 +00:00
}
2017-08-11 22:41:02 +00:00
async function listGroupedDTAjax ( context , listId , params ) {
2017-08-19 13:12:22 +00:00
return await knex . transaction ( async tx => {
2018-07-31 04:34:28 +00:00
await shares . enforceEntityPermissionTx ( tx , context , 'list' , listId , 'viewFields' ) ;
2017-08-19 13:12:22 +00:00
return await dtHelpers . ajaxListTx (
tx ,
params ,
builder => builder
. from ( 'custom_fields' )
. where ( 'custom_fields.list' , listId )
. whereIn ( 'custom_fields.type' , groupedTypes ) ,
[ 'custom_fields.id' , 'custom_fields.name' , 'custom_fields.type' , 'custom_fields.key' , 'custom_fields.order_list' ] ,
{
orderByBuilder : ( builder , orderColumn , orderDir ) => {
if ( orderColumn === 'custom_fields.order_list' ) {
builder
. orderBy ( knex . raw ( '-custom_fields.order_list' ) , orderDir === 'asc' ? 'desc' : 'asc' ) // This is MySQL speciality. It sorts the rows in ascending order with NULL values coming last
. orderBy ( 'custom_fields.name' , orderDir ) ;
} else {
builder
. orderBy ( orderColumn , orderDir )
. orderBy ( 'custom_fields.name' , orderDir ) ;
}
2017-08-11 22:41:02 +00:00
}
}
2017-08-19 13:12:22 +00:00
) ;
} ) ;
2017-08-11 22:41:02 +00:00
}
2017-08-11 06:51:30 +00:00
async function serverValidate ( context , listId , data ) {
2017-08-13 18:11:58 +00:00
return await knex . transaction ( async tx => {
const result = { } ;
2017-08-11 06:51:30 +00:00
await shares . enforceEntityPermissionTx ( tx , context , 'list' , listId , 'manageFields' ) ;
if ( data . key ) {
2017-08-11 16:16:44 +00:00
const existingKeyQuery = tx ( 'custom_fields' ) . where ( {
2017-08-11 06:51:30 +00:00
list : listId ,
key : data . key
2017-08-11 16:16:44 +00:00
} ) ;
if ( data . id ) {
existingKeyQuery . whereNot ( 'id' , data . id ) ;
}
2017-08-11 06:51:30 +00:00
2017-08-11 16:16:44 +00:00
const existingKey = await existingKeyQuery . first ( ) ;
2017-08-11 06:51:30 +00:00
result . key = {
2017-08-11 16:16:44 +00:00
exists : ! ! existingKey
2017-08-11 06:51:30 +00:00
} ;
}
2017-08-13 18:11:58 +00:00
return result ;
} ) ;
2017-08-11 06:51:30 +00:00
}
async function _validateAndPreprocess ( tx , listId , entity , isCreate ) {
enforce ( entity . type === 'option' || ! entity . group , 'Only option may have a group assigned' ) ;
enforce ( entity . type !== 'option' || entity . group , 'Option must have a group assigned.' ) ;
2017-08-13 09:32:31 +00:00
enforce ( entity . type !== 'option' || ( entity . orderListBefore === 'none' && entity . orderSubscribeBefore === 'none' && entity . orderManageBefore === 'none' ) , 'Option cannot be made visible' ) ;
2017-08-11 06:51:30 +00:00
enforce ( ! entity . group || await tx ( 'custom_fields' ) . where ( { list : listId , id : entity . group } ) . first ( ) , 'Group field does not exist' ) ;
enforce ( entity . name , 'Name must be present' ) ;
const fieldType = fieldTypes [ entity . type ] ;
enforce ( fieldType , 'Unknown field type' ) ;
const validateErrs = fieldType . validate ( entity ) ;
enforce ( validators . mergeTagValid ( entity . key ) , 'Merge tag is not valid.' ) ;
2017-08-20 21:44:33 +00:00
const existingWithKeyQuery = tx ( 'custom_fields' ) . where ( {
2017-08-11 06:51:30 +00:00
list : listId ,
key : entity . key
} ) ;
if ( ! isCreate ) {
2017-08-11 16:16:44 +00:00
existingWithKeyQuery . whereNot ( 'id' , entity . id ) ;
2017-08-11 06:51:30 +00:00
}
const existingWithKey = await existingWithKeyQuery . first ( ) ;
if ( existingWithKey ) {
throw new interoperableErrors . DuplicitKeyError ( ) ;
}
entity . settings = JSON . stringify ( entity . settings ) ;
}
async function _sortIn ( tx , listId , entityId , orderListBefore , orderSubscribeBefore , orderManageBefore ) {
const flds = await tx ( 'custom_fields' ) . where ( 'list' , listId ) . whereNot ( 'id' , entityId ) ;
const order = { } ;
for ( const row of flds ) {
order [ row . id ] = {
order _list : null ,
order _subscribe : null ,
order _manage : null
} ;
}
order [ entityId ] = {
order _list : null ,
order _subscribe : null ,
order _manage : null
} ;
function computeOrder ( fldName , sortInBefore ) {
flds . sort ( ( x , y ) => x [ fldName ] - y [ fldName ] ) ;
const ids = flds . filter ( x => x [ fldName ] !== null ) . map ( x => x . id ) ;
let sortedIn = false ;
let idx = 1 ;
for ( const id of ids ) {
if ( sortInBefore === id ) {
order [ entityId ] [ fldName ] = idx ;
sortedIn = true ;
idx += 1 ;
}
order [ id ] [ fldName ] = idx ;
idx += 1 ;
}
if ( ! sortedIn && sortInBefore !== 'none' ) {
order [ entityId ] [ fldName ] = idx ;
}
}
computeOrder ( 'order_list' , orderListBefore ) ;
computeOrder ( 'order_subscribe' , orderSubscribeBefore ) ;
computeOrder ( 'order_manage' , orderManageBefore ) ;
for ( const id in order ) {
await tx ( 'custom_fields' ) . where ( { list : listId , id } ) . update ( order [ id ] ) ;
}
}
async function create ( context , listId , entity ) {
2017-08-13 18:11:58 +00:00
return await knex . transaction ( async tx => {
2017-08-11 06:51:30 +00:00
await shares . enforceEntityPermissionTx ( tx , context , 'list' , listId , 'manageFields' ) ;
await _validateAndPreprocess ( tx , listId , entity , true ) ;
2017-08-11 16:16:44 +00:00
const fieldType = fieldTypes [ entity . type ] ;
2017-08-11 06:51:30 +00:00
let columnName ;
if ( ! fieldType . grouped ) {
2017-08-11 16:16:44 +00:00
columnName = ( 'custom_' + slugify ( entity . name , '_' ) + '_' + shortid . generate ( ) ) . toLowerCase ( ) . replace ( /[^a-z0-9_]/g , '' ) ;
2017-08-11 06:51:30 +00:00
}
const filteredEntity = filterObject ( entity , allowedKeysCreate ) ;
filteredEntity . list = listId ;
filteredEntity . column = columnName ;
const ids = await tx ( 'custom_fields' ) . insert ( filteredEntity ) ;
2017-08-13 18:11:58 +00:00
const id = ids [ 0 ] ;
2017-08-13 09:32:31 +00:00
2017-08-11 16:16:44 +00:00
await _sortIn ( tx , listId , id , entity . orderListBefore , entity . orderSubscribeBefore , entity . orderManageBefore ) ;
2017-08-11 06:51:30 +00:00
if ( columnName ) {
await knex . schema . table ( 'subscription__' + listId , table => {
fieldType . addColumn ( table , columnName ) ;
if ( fieldType . indexed ) {
table . index ( columnName ) ;
}
} ) ;
}
2017-08-13 18:11:58 +00:00
return id ;
} ) ;
2017-08-11 06:51:30 +00:00
}
async function updateWithConsistencyCheck ( context , listId , entity ) {
await knex . transaction ( async tx => {
await shares . enforceEntityPermissionTx ( tx , context , 'list' , listId , 'manageFields' ) ;
const existing = await tx ( 'custom_fields' ) . where ( { list : listId , id : entity . id } ) . first ( ) ;
if ( ! existing ) {
throw new interoperableErrors . NotFoundError ( ) ;
}
2017-08-20 21:44:33 +00:00
existing . settings = JSON . parse ( existing . settings ) ;
2017-08-11 06:51:30 +00:00
const existingHash = hash ( existing ) ;
if ( existingHash !== entity . originalHash ) {
throw new interoperableErrors . ChangedError ( ) ;
}
enforce ( entity . type === existing . type , 'Field type cannot be changed' ) ;
2017-08-11 16:16:44 +00:00
await _validateAndPreprocess ( tx , listId , entity , false ) ;
2017-08-11 06:51:30 +00:00
2017-08-20 21:44:33 +00:00
await tx ( 'custom_fields' ) . where ( { list : listId , id : entity . id } ) . update ( filterObject ( entity , allowedKeysUpdate ) ) ;
2017-08-11 16:16:44 +00:00
await _sortIn ( tx , listId , entity . id , entity . orderListBefore , entity . orderSubscribeBefore , entity . orderManageBefore ) ;
2017-08-11 06:51:30 +00:00
} ) ;
}
2017-08-14 20:53:29 +00:00
async function removeTx ( tx , context , listId , id ) {
await shares . enforceEntityPermissionTx ( tx , context , 'list' , listId , 'manageFields' ) ;
2017-08-11 06:51:30 +00:00
2017-08-14 20:53:29 +00:00
const existing = await tx ( 'custom_fields' ) . where ( { list : listId , id : id } ) . first ( ) ;
if ( ! existing ) {
throw new interoperableErrors . NotFoundError ( ) ;
}
2017-08-11 06:51:30 +00:00
2017-08-14 20:53:29 +00:00
const fieldType = fieldTypes [ existing . type ] ;
2017-08-11 06:51:30 +00:00
2017-08-14 20:53:29 +00:00
await tx ( 'custom_fields' ) . where ( { list : listId , id } ) . del ( ) ;
2017-08-11 06:51:30 +00:00
2017-08-14 20:53:29 +00:00
if ( fieldType . grouped ) {
await tx ( 'custom_fields' ) . where ( { list : listId , group : id } ) . del ( ) ;
2017-08-11 06:51:30 +00:00
2017-08-14 20:53:29 +00:00
} else {
await knex . schema . table ( 'subscription__' + listId , table => {
table . dropColumn ( existing . column ) ;
} ) ;
2017-08-11 06:51:30 +00:00
2017-08-19 13:12:22 +00:00
await segments . removeRulesByColumnTx ( tx , context , listId , existing . column ) ;
2017-08-14 20:53:29 +00:00
}
}
async function remove ( context , listId , id ) {
await knex . transaction ( async tx => {
await removeTx ( tx , context , listId , id ) ;
2017-08-11 06:51:30 +00:00
} ) ;
}
2017-07-13 11:27:03 +00:00
2017-08-14 20:53:29 +00:00
async function removeAllByListIdTx ( tx , context , listId ) {
const entities = await tx ( 'custom_fields' ) . where ( 'list' , listId ) . select ( [ 'id' ] ) ;
for ( const entity of entities ) {
await removeTx ( tx , context , listId , entity . id ) ;
}
}
2018-01-28 22:59:05 +00:00
// Returns an array that can be used for rendering by Handlebars
async function forHbs ( context , listId , subscription ) { // assumes grouped subscription
2017-12-10 20:44:35 +00:00
const customFields = [ {
name : 'Email Address' ,
column : 'email' ,
2018-01-28 22:59:05 +00:00
key : 'EMAIL' ,
2017-12-10 20:44:35 +00:00
typeSubscriptionEmail : true ,
value : subscription ? subscription . email : '' ,
order _subscribe : - 1 ,
order _manage : - 1
} ] ;
2018-01-28 22:59:05 +00:00
const flds = await listGrouped ( context , listId ) ;
2017-12-10 20:44:35 +00:00
for ( const fld of flds ) {
2018-01-28 22:59:05 +00:00
const type = fieldTypes [ fld . type ] ;
2018-09-01 19:29:10 +00:00
const fldCol = getFieldColumn ( fld ) ;
2018-01-28 22:59:05 +00:00
const entry = {
name : fld . name ,
key : fld . key ,
[ type . getHbsType ( fld ) ] : true ,
order _subscribe : fld . order _subscribe ,
order _manage : fld . order _manage ,
} ;
if ( ! type . grouped && ! type . enumerated ) {
2018-09-01 19:29:10 +00:00
// subscription[fldCol] may not exists because we are getting the data from "fromPost"
entry . value = ( subscription ? type . forHbs ( fld , subscription [ fldCol ] ) : null ) || '' ;
2018-01-28 22:59:05 +00:00
} else if ( type . grouped ) {
const options = [ ] ;
2018-09-01 19:29:10 +00:00
const value = ( subscription ? subscription [ fldCol ] : null ) || ( type . cardinality === Cardinality . SINGLE ? null : [ ] ) ;
2018-01-28 22:59:05 +00:00
for ( const optCol in fld . groupedOptions ) {
const opt = fld . groupedOptions [ optCol ] ;
let isEnabled ;
if ( type . cardinality === Cardinality . SINGLE ) {
isEnabled = value === opt . column ;
} else {
isEnabled = value . includes ( opt . column ) ;
}
options . push ( {
key : opt . key ,
name : opt . name ,
value : isEnabled
} ) ;
}
entry . options = options ;
} else if ( type . enumerated ) {
const options = [ ] ;
2018-09-01 19:29:10 +00:00
const value = ( subscription ? subscription [ fldCol ] : null ) || null ;
2018-01-28 22:59:05 +00:00
for ( const opt of fld . settings . options ) {
options . push ( {
key : opt . key ,
name : opt . label ,
value : value === opt . key
} ) ;
}
entry . options = options ;
2017-12-10 20:44:35 +00:00
}
2018-01-28 22:59:05 +00:00
customFields . push ( entry ) ;
2017-12-10 20:44:35 +00:00
}
return customFields ;
}
2018-09-01 19:29:10 +00:00
// Converts subscription data received via (1) POST request from subscription form, (2) via subscribe request to API v1 to subscription structure supported by subscriptions model,
// or (3) from import.
// If a field is not specified in the POST data, it is also omitted in the returned subscription
function _fromText ( listId , data , flds , isGrouped , keyName , singleCardUsesKeyName ) {
2018-01-28 22:59:05 +00:00
const subscription = { } ;
2018-09-01 19:29:10 +00:00
if ( isGrouped ) {
for ( const fld of flds ) {
const fldKey = fld [ keyName ] ;
if ( fldKey && fldKey in data ) {
const type = fieldTypes [ fld . type ] ;
const fldCol = getFieldColumn ( fld ) ;
let value = null ;
if ( ! type . grouped && ! type . enumerated ) {
value = type . parsePostValue ( fld , cleanupFromPost ( data [ fldKey ] ) ) ;
} else if ( type . grouped ) {
if ( type . cardinality === Cardinality . SINGLE ) {
for ( const optCol in fld . groupedOptions ) {
const opt = fld . groupedOptions [ optCol ] ;
const optKey = opt [ keyName ] ;
// This handles two different formats for grouped dropdowns and radios.
// The first part of the condition handles the POST requests from the subscription form, while the
// second part handles the subscribe request to API v1
if ( singleCardUsesKeyName ) {
if ( data [ fldKey ] === optKey ) {
value = opt . column
}
} else {
const optType = fieldTypes [ opt . type ] ;
const optValue = optType . parsePostValue ( fld , cleanupFromPost ( data [ optKey ] ) ) ;
if ( optValue ) {
value = opt . column
}
}
2018-05-21 17:41:10 +00:00
}
2018-09-01 19:29:10 +00:00
} else {
value = [ ] ;
2018-01-28 22:59:05 +00:00
2018-09-01 19:29:10 +00:00
for ( const optCol in fld . groupedOptions ) {
const opt = fld . groupedOptions [ optCol ] ;
const optKey = opt [ keyName ] ;
const optType = fieldTypes [ opt . type ] ;
const optValue = optType . parsePostValue ( fld , cleanupFromPost ( data [ optKey ] ) ) ;
2018-01-28 22:59:05 +00:00
2018-09-01 19:29:10 +00:00
if ( optValue ) {
value . push ( opt . column ) ;
}
2018-05-21 17:41:10 +00:00
}
2018-01-28 22:59:05 +00:00
}
2018-09-01 19:29:10 +00:00
} else if ( type . enumerated ) {
value = data [ fldKey ] ;
2018-01-28 22:59:05 +00:00
}
2018-05-21 17:41:10 +00:00
2018-09-01 19:29:10 +00:00
subscription [ fldCol ] = value ;
2018-01-28 22:59:05 +00:00
}
2018-09-01 19:29:10 +00:00
}
} else {
for ( const fld of flds ) {
const fldKey = fld [ keyName ] ;
if ( fldKey && fldKey in data ) {
const type = fieldTypes [ fld . type ] ;
const fldCol = getFieldColumn ( fld ) ;
2018-01-28 22:59:05 +00:00
2018-09-01 19:29:10 +00:00
subscription [ fldCol ] = type . parsePostValue ( fld , cleanupFromPost ( data [ fldKey ] ) ) ;
}
2018-01-28 22:59:05 +00:00
}
}
return subscription ;
}
2018-09-01 19:29:10 +00:00
async function fromPost ( context , listId , data ) { // assumes grouped subscription and indexation by merge key
const flds = await listGrouped ( context , listId ) ;
return _fromText ( listId , data , flds , true , 'key' , true ) ;
}
async function fromAPI ( context , listId , data ) { // assumes grouped subscription and indexation by merge key
const flds = await listGrouped ( context , listId ) ;
return _fromText ( listId , data , flds , true , 'key' , false ) ;
}
function fromImport ( listId , flds , data ) { // assumes ungrouped subscription and indexation by column
return _fromText ( listId , data , flds , true , 'column' , false ) ;
}
2018-01-28 22:59:05 +00:00
2017-08-19 13:12:22 +00:00
// This is to handle circular dependency with segments.js
Object . assign ( module . exports , {
2017-08-20 21:44:33 +00:00
Cardinality ,
getFieldType ,
2017-08-11 06:51:30 +00:00
hash ,
getById ,
list ,
2017-08-19 13:12:22 +00:00
listTx ,
2017-08-20 21:44:33 +00:00
listGrouped ,
listGroupedTx ,
2017-08-13 18:11:58 +00:00
listByOrderListTx ,
2017-08-11 06:51:30 +00:00
listDTAjax ,
2017-08-11 22:41:02 +00:00
listGroupedDTAjax ,
2017-08-11 06:51:30 +00:00
create ,
updateWithConsistencyCheck ,
remove ,
2017-08-14 20:53:29 +00:00
removeAllByListIdTx ,
2017-12-10 20:44:35 +00:00
serverValidate ,
2018-01-28 22:59:05 +00:00
forHbs ,
2018-09-01 19:29:10 +00:00
fromPost ,
fromAPI ,
fromImport
2017-08-19 13:12:22 +00:00
} ) ;